Compare commits

..
1269 changed files with 16212 additions and 31165 deletions
Generated
+112 -390
View File
File diff suppressed because it is too large Load Diff
+4 -13
View File
@@ -30,16 +30,13 @@ members = [
"crates/lib/actix_cors_configs",
"crates/lib/actix_helpers",
"crates/lib/api_clients/cloud_storage",
"crates/lib/api_clients/fal_client",
"crates/lib/api_clients/google_drive_common",
"crates/lib/api_clients/google_sign_in",
"crates/lib/api_clients/openai_sora_client",
"crates/lib/api_clients/runwayml_client",
"crates/lib/api_clients/storyteller_client",
"crates/lib/api_clients/thumbnail_generator",
"crates/lib/bootstrap",
"crates/lib/bucket_paths",
"crates/lib/build_metadata",
"crates/lib/caching/memory_caching",
"crates/lib/caching/memory_store",
"crates/lib/caching/redis_caching",
@@ -57,7 +54,6 @@ members = [
"crates/lib/email_sender",
"crates/lib/errors",
"crates/lib/files/archives",
"crates/lib/files/file_to_base64_url",
"crates/lib/files/filesys",
"crates/lib/files/hashing",
"crates/lib/files/images",
@@ -67,7 +63,6 @@ members = [
"crates/lib/idempotency",
"crates/lib/image_algorithms",
"crates/lib/jobs_common",
"crates/lib/jwt_light",
"crates/lib/logging",
"crates/lib/machine_learning/ml_models",
"crates/lib/machine_learning/ml_weights_registry",
@@ -162,16 +157,15 @@ workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
# External libs
# Centrally pinned versions so that we can uniformly upgrade/downgrade versions across the workspace
# If a crate needs to use an older/newer version, the dependency should be managed in that crate's own Cargo.toml
anyhow = "1.0.98"
anyhow = "1.0.97"
base64 = "0.22.1"
chrono = { version = "0.4.41", features = ["serde"] }
chrono = { version = "0.4.40", features = ["serde"] }
cookie = "0.18.1"
futures = { version = "0.3.31" }
hex-literal = "1.0.0"
indexmap = "2.9.0"
log = "0.4.27"
once_cell = "1.21.3"
rand = "0.9.1"
rand = "0.9.0"
regex = "1.11.1"
reqwest = { version = "0.12.15", features = ["json", "cookies"] }
serde = { version = "1.0.219", features = ["derive"] }
@@ -181,8 +175,7 @@ serde_yaml = "0.9.1"
sha3 = "0.10.8"
tauri = { version = "2.4.1", features = ["unstable", "devtools"] } # NB: "devtools" prevents App Store distribution
thiserror = "2.0.12"
tokio = { version = "1.45.0", features = ["macros"] }
toml = "0.8.22"
toml = "0.8.20"
url = { version = "2.5.4", features = ["serde"] }
# Internal libs
@@ -190,11 +183,9 @@ crockford = { path = "crates/lib/data/crockford" }
enums = { path = "crates/_types/enums" }
enums_public = { path = "crates/_types/enums_public" }
errors = { path = "crates/lib/errors" }
fal_client = { path = "crates/lib/api_clients/fal_client" }
filesys = { path = "crates/lib/files/filesys" }
idempotency = { path = "crates/lib/idempotency" }
images = { path = "crates/lib/files/images" }
jwt_light = { path = "crates/lib/jwt_light" }
memory_caching = { path = "crates/lib/caching/memory_caching" }
memory_store = { path = "crates/lib/caching/memory_store" }
mimetypes = { path = "crates/lib/files/mimetypes" }
-3
View File
@@ -25,11 +25,9 @@ workspace-build-acceleration = { workspace = true }
# Internal
crockford = { workspace = true }
errors = { workspace = true }
fal_client = { workspace = true }
filesys = { workspace = true }
memory_caching = { workspace = true }
memory_store = { workspace = true }
mimetypes = { workspace = true }
openai_sora_client = { workspace = true }
primitives = { workspace = true }
storyteller_client = { workspace = true }
@@ -62,7 +60,6 @@ tempdir = "0.3.7"
tempfile = "3.18.0"
tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "fs"] }
tokio-util = { version = "0.7", features = ["codec"] }
url = { workspace = true }
# NB: We need to vendor this in so that we can access the cookies inside via the private API
tauri-plugin-http = { path = "../../vendor/tauri-plugin-http" }
@@ -10,12 +10,19 @@
"log:default",
"upload:allow-download",
"upload:default",
"http:allow-fetch",
{
"identifier": "http:default",
"allow": [
{
"url": "https://*"
"url": "https://*.storyteller.ai"
},
{
"url": "https://*.fakeyou.com"
}
],
"deny": [
{
"url": "https://private.tauri.app"
}
]
},
@@ -1,5 +1,5 @@
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use errors::AnyhowResult;
use log::{error, info};
use serde_derive::Serialize;
@@ -17,15 +17,7 @@ pub struct AppPreferencesPayload {
/// Play sounds on events.
pub play_sounds: bool,
/// Key pointing to file; defined in the frontend code.
/// Defined for enqueue since image enqueue can be async
pub enqueue_success_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
/// Defined for enqueue since image enqueue can be async
pub enqueue_failure_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
pub generation_success_sound: Option<String>,
@@ -33,7 +25,6 @@ pub struct AppPreferencesPayload {
pub generation_failure_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
#[deprecated]
pub generation_enqueue_sound: Option<String>,
}
@@ -61,8 +52,6 @@ async fn get_prefs(app_prefs: &AppPreferencesManager) -> AnyhowResult<AppPrefere
Ok(AppPreferencesPayload {
preferred_download_directory: prefs.preferred_download_directory,
play_sounds: prefs.play_sounds,
enqueue_success_sound: prefs.enqueue_success_sound,
enqueue_failure_sound: prefs.enqueue_failure_sound,
generation_success_sound: prefs.generation_success_sound,
generation_failure_sound: prefs.generation_failure_sound,
generation_enqueue_sound: prefs.generation_enqueue_sound,
@@ -1,6 +1,6 @@
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::state::data_dir::app_data_root::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{error, info};
@@ -29,10 +29,9 @@ pub enum ValueType {
pub enum PreferenceName {
PreferredDownloadDirectory,
PlaySounds,
EnqueueSuccessSound,
EnqueueFailureSound,
GenerationSuccessSound,
GenerationFailureSound,
GenerationEnqueueSound,
}
#[derive(Serialize)]
@@ -86,16 +85,6 @@ async fn update_prefs(
return Err(anyhow!("Invalid value: {:?}", request.value)),
}
}
PreferenceName::EnqueueSuccessSound => {
prefs.enqueue_success_sound = request.value
.map(|val| string_value(&val))
.transpose()?;
}
PreferenceName::EnqueueFailureSound => {
prefs.enqueue_failure_sound = request.value
.map(|val| string_value(&val))
.transpose()?;
}
PreferenceName::GenerationSuccessSound => {
prefs.generation_success_sound = request.value
.map(|val| string_value(&val))
@@ -106,6 +95,11 @@ async fn update_prefs(
.map(|val| string_value(&val))
.transpose()?;
}
PreferenceName::GenerationEnqueueSound => {
prefs.generation_enqueue_sound = request.value
.map(|val| string_value(&val))
.transpose()?;
}
}
app_prefs.set_clone(&prefs)?;
@@ -1,4 +1,4 @@
pub mod app_preferences;
pub mod command_response_wrapper;
pub mod flip_image;
pub mod platform_info_command;
pub mod sora;
@@ -1,4 +1,4 @@
use crate::core::state::os_platform::OsPlatform;
use crate::state::os_platform::OsPlatform;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
@@ -1,5 +1,3 @@
pub mod check_sora_session_command;
pub mod open_sora_login_command;
pub mod sora_image_generation_command;
pub mod sora_image_remix_command;
pub mod sora_logout_command;
@@ -0,0 +1,72 @@
use crate::threads::sora_session_login_thread::{LOGIN_WINDOW_NAME, SORA_LOGIN_URL};
use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies;
use errors::AnyhowResult;
use log::{error, info};
use once_cell::sync::Lazy;
use std::time::Duration;
use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowBuilder};
use tokio::time::sleep_until;
pub const START_URL_STR: &str = "https://storyteller.ai/";
pub static START_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(START_URL_STR).expect("URL should parse")
});
pub const SORA_HOMEPAGE_URL_STR: &str = "https://sora.com/";
pub static SORA_HOMEPAGE_URL : Lazy<Url> = Lazy::new(|| {
Url::parse(SORA_HOMEPAGE_URL_STR).expect("URL should parse")
});
#[tauri::command]
pub async fn open_sora_login_command(
app: AppHandle,
) -> Result<String, String> {
info!("open_login_command called");
do_open_login(&app)
.await
.map_err(|err| {
error!("Error opening login: {:?}", err);
format!("Error opening login: {:?}", err)
})?;
Ok("result".to_string())
}
async fn do_open_login(app: &AppHandle) -> AnyhowResult<()> {
info!("Building login window...");
let url = WebviewUrl::External(START_URL.clone());
let window = WebviewWindowBuilder::new(app, LOGIN_WINDOW_NAME, url)
//.user_agent(openai_sora_client::credentials::USER_AGENT)
.always_on_top(true)
.title("Login to OpenAI")
.center()
.resizable(true)
.visible(true)
.closable(true)
.focused(true)
.devtools(true)
.build()?;
for (name, webview) in window.webviews() {
if name != LOGIN_WINDOW_NAME {
continue;
}
clear_all_webview_cookies(&webview)?;
// NB: We're starting to get Cloudflare protection screens. Let's try to avoid.
webview.navigate(SORA_HOMEPAGE_URL.clone())?;
tokio::time::sleep(Duration::from_millis(100)).await;
webview.navigate(SORA_LOGIN_URL.clone())?;
break;
}
info!("Done.");
Ok(())
}
@@ -1,6 +1,6 @@
use crate::services::sora::state::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::services::sora::state::sora_credential_holder::SoraCredentialHolder;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use errors::AnyhowResult;
@@ -14,13 +14,8 @@ use openai_sora_client::requests::image_gen::sora_image_gen_remix::{sora_image_g
use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
use std::fs::read_to_string;
use std::io::Cursor;
use std::time::Duration;
use tauri::{AppHandle, Manager, State};
const SORA_IMAGE_UPLOAD_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds
const SORA_IMAGE_REMIX_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds
#[tauri::command]
pub async fn sora_image_generation_command(
prompt: &str,
@@ -60,13 +55,8 @@ pub async fn generate_image(
let image_bytes = BASE64_STANDARD.decode(image)?;
let filename = "image.png".to_string();
let response = sora_media_upload_from_bytes(
image_bytes,
filename,
CredentialMigrationRef::New(&creds),
Some(SORA_IMAGE_UPLOAD_TIMEOUT))
let response = sora_media_upload_from_bytes(image_bytes, filename, CredentialMigrationRef::New(&creds))
.await
.map_err(|err| {
error!("Failed to upload image to Sora: {:?}", err);
@@ -82,7 +72,6 @@ pub async fn generate_image(
image_size: ImageSize::Square,
sora_media_tokens: sora_media_tokens.clone(),
credentials: CredentialMigrationRef::New(&creds),
request_timeout: Some(SORA_IMAGE_REMIX_TIMEOUT),
}).await
.map_err(|err| {
error!("Failed to call Sora image generation: {:?}", err);
@@ -0,0 +1,174 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::state::sora::sora_task_queue::SoraTaskQueue;
use crate::utils::get_url_file_extension::get_url_file_extension;
use crate::utils::simple_http_download::simple_http_download;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use errors::AnyhowResult;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{DynamicImage, ImageReader};
use log::{debug, error, info};
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::creds::credential_migration::CredentialMigrationRef;
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use openai_sora_client::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file;
use serde_derive::{Deserialize, Serialize};
use std::fs::read_to_string;
use std::io::Cursor;
use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::threads::sora_task_polling_thread::SoraImageGenerationComplete;
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageEnqueueFailure {
// TODO: Reason.
}
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageEnqueueSuccess {
}
#[derive(Deserialize)]
pub struct SoraImageRemixCommand {
/// Image media file; the engine or canvas snapshot (screenshot).
pub snapshot_media_token: MediaFileToken,
/// The user's image generation prompt.
pub prompt: String,
/// Turn off the system prompt.
pub disable_system_prompt: Option<bool>,
/// Additional images to include (optional). Up to nine images.
pub maybe_additional_images: Option<Vec<MediaFileToken>>,
pub maybe_number_of_samples: Option<u32>,
}
#[tauri::command]
pub async fn sora_image_remix_command(
app: AppHandle,
request: SoraImageRemixCommand,
app_data_root: State<'_, AppDataRoot>,
sora_creds_manager: State<'_, SoraCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
) -> Result<String, String> {
info!("image_generation_command called; scene media token: {:?}, additional images: {:?}",
request.snapshot_media_token, request.maybe_additional_images);
// TODO(bt,2025-04-24): Better error messages to caller
let result = generate_image(request, &app_data_root, &sora_creds_manager, &sora_task_queue).await;
if let Err(err) = result {
error!("error: {:?}", err);
let result = app.emit("sora-image-enqueue-failure", SoraImageEnqueueFailure{});
if let Err(err) = result {
error!("Failed to emit event: {:?}", err);
}
return Err("there was an error".to_string());
}
let result = app.emit("sora-image-enqueue-success", SoraImageEnqueueSuccess{});
if let Err(err) = result {
error!("Failed to emit event: {:?}", err);
}
Ok("success".to_string())
}
pub async fn generate_image(
request: SoraImageRemixCommand,
app_data_root: &AppDataRoot,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> AnyhowResult<()> {
let response = get_media_file(&ApiHost::Storyteller, &request.snapshot_media_token).await?;
let media_file_url = &response.media_file.media_links.cdn_url;
let extension_with_dot = get_url_file_extension(media_file_url)
.map(|ext| format!(".{}", ext))
.unwrap_or_else(|| ".png".to_string());
let filename = format!("{}{}", response.media_file.token.as_str(), extension_with_dot);
let filename = app_data_root.downloads_dir().path().join(&filename);
simple_http_download(&media_file_url, &filename).await?;
let files_to_upload = vec![filename];
let mut creds = sora_creds_manager.get_credentials_required()?;
let credential_updated = maybe_upgrade_or_renew_session(&mut creds)
.await
.map_err(|err| {
error!("Failed to upgrade or renew session: {:?}", err);
err
})?;
if credential_updated {
info!("Storing updated credentials");
sora_creds_manager.set_credentials(&creds)?;
}
let mut sora_media_tokens = Vec::with_capacity(files_to_upload.len());
for (i, file_path) in files_to_upload.iter().enumerate() {
info!("Uploading image {} of {}...", (i+1), files_to_upload.len());
let (response, maybe_new_credentials) =
image_upload_from_file_with_session_auto_renew(ImageUploadFromFileAutoRenewRequest {
file_path,
credentials: &creds,
}).await?;
if let Some(new_creds) = maybe_new_credentials {
info!("Storing updated credentials.");
sora_creds_manager.set_credentials(&new_creds)?;
creds = new_creds;
}
sora_media_tokens.push(response.id);
}
info!("Calling image generation...");
// TODO(bt,2025-04-21): Download media tokens.
// Note: This is incredibly inefficient. We should keep a local cache.
// Also, if they've already been uploaded to OpenAI, we shouldn't continue to re-upload.
let (response, maybe_new_creds) =
image_remix_with_session_auto_renew(ImageRemixAutoRenewRequest {
prompt: request.prompt.to_string(),
num_images: NumImages::One,
image_size: ImageSize::Square,
sora_media_tokens: sora_media_tokens.clone(),
credentials: &creds,
}).await?;
if let Some(new_creds) = maybe_new_creds {
info!("Storing updated credentials.");
sora_creds_manager.set_credentials(&new_creds)?;
}
info!("New Sora Task ID: {:?} ", response.task_id);
sora_task_queue.insert(&response.task_id)?;
Ok(())
}
@@ -1,132 +0,0 @@
use serde::Serialize;
use std::error::Error;
use std::str::FromStr;
/// Easy to use Result<T, E> type.
pub type CommandResult<SuccessPayload, ErrType, ErrPayload> =
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<ErrType, ErrPayload>>;
/// No inner payloads for this type. Just strings as messages.
pub type SimpleCommandResult = CommandResult<(), (), ()>;
/// Statuses for successful commands.
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum CommandSuccessStatus {
Success, // 200
}
/// Statuses for errorful commands.
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum CommandErrorStatus {
BadRequest, // 400
Unauthorized, // 401
TooManyRequests, // 429
ServerError, // 500
}
#[derive(Serialize, Debug)]
pub struct CommandSuccessResponseWrapper<T: Serialize> {
/// Both "success" and "error" types have a `status` field for the frontend.
pub status: CommandSuccessStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub success_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payload: Option<T>,
}
#[derive(Serialize, Debug)]
pub struct CommandErrorResponseWrapper<ErrType: Serialize, ErrPayload: Serialize> {
/// Both "success" and "error" types have a `status` field for the frontend.
pub status: CommandErrorStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
/// Represents a "type" of error, preferably an enum without payloads.
/// Preferably this serializes to a string.
#[serde(skip_serializing_if = "Option::is_none")]
pub error_type: Option<ErrType>,
/// Represents a full error details object or payload.
/// This could be a structure, message, or whatever.
#[serde(skip_serializing_if = "Option::is_none")]
pub error_details: Option<ErrPayload>,
}
impl CommandSuccessResponseWrapper<()> {
pub fn empty() -> Self {
CommandSuccessResponseWrapper {
status: CommandSuccessStatus::Success,
success_message: None,
payload: None,
}
}
}
/// NB: This is a hack because of `()` (a.k.a. the "unit" type) and `Serialize` trait coherence and overlap.
/// We cannot currently implement a trait for `()` and generic `Serialize` because `()` itself implements
/// `Serialize`. Rust's `"specialization"`, which would let us account for this, is an unstable, nightly-only
/// feature. So instead we manually mark all types other than `()` with `SerializeMarker` as a hack to
/// distinguish the two.
pub trait SerializeMarker : Serialize {}
// NB: () is Serialize! So we can't broadly implement just `T: Serialize`.
impl From<()> for CommandSuccessResponseWrapper<()> {
fn from(_: ()) -> Self {
CommandSuccessResponseWrapper {
status: CommandSuccessStatus::Success,
success_message: None,
payload: None,
}
}
}
/// NB: Note the `"+ SerializeMarker"` bound here. This is a hack to distinguish between `()` and other types
/// for which we will manually mark as `SerializeMarker`.
impl <T> From<T> for CommandSuccessResponseWrapper<T> where T: Serialize + SerializeMarker {
fn from(val: T) -> Self {
CommandSuccessResponseWrapper {
status: CommandSuccessStatus::Success,
success_message: None,
payload: Some(val),
}
}
}
impl From<&str> for CommandSuccessResponseWrapper<String> {
fn from(msg: &str) -> Self {
CommandSuccessResponseWrapper {
status: CommandSuccessStatus::Success,
success_message: Some(msg.to_string()),
payload: None,
}
}
}
impl From<&str> for CommandErrorResponseWrapper<(), ()> {
fn from(value: &str) -> Self {
CommandErrorResponseWrapper {
status: CommandErrorStatus::BadRequest, // NB: Default to 500 error.
error_message: Some(value.to_string()),
error_type: None,
error_details: None,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_empty_tuple_serialize() {
let response = super::CommandSuccessResponseWrapper::<()>::from(());
let serialized = serde_json::to_string(&response).unwrap();
// NB: This tests that the blanket "Serialize" impl doesn't produce a "null payload" field.
assert_eq!(serialized, r#"{"status":"success"}"#);
}
}
@@ -1,3 +0,0 @@
pub mod notification_event;
pub mod sendable_event_error;
pub mod sendable_event_trait;
@@ -1,31 +0,0 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum SendableEventError {
TauriError(tauri::Error),
AnyhowError(anyhow::Error),
}
impl Error for SendableEventError {}
impl Display for SendableEventError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::TauriError(err) => write!(f, "SendableEventError::TauriError: {}", err),
Self::AnyhowError(err) => write!(f, "SendableEventError::AnyhowError: {}", err),
}
}
}
impl From<anyhow::Error> for SendableEventError {
fn from(err: anyhow::Error) -> Self {
SendableEventError::AnyhowError(err)
}
}
impl From<tauri::Error> for SendableEventError {
fn from(err: tauri::Error) -> Self {
SendableEventError::TauriError(err)
}
}
@@ -1,21 +0,0 @@
use log::info;
use crate::core::events::sendable_event_error::SendableEventError;
use serde::Serialize;
use tauri::{AppHandle, Emitter};
/// Implement this on a Serializable object to be able to send it as an event to the frontend.
pub trait SendableEvent : Serialize {
/// This is the name of the event that the frontend subscribes to.
const FRONTEND_EVENT_NAME: &'static str;
/// Default implementation of send().
/// This serializes and sends the event to the frontend.
fn send(&self, app: &AppHandle) -> Result<(), SendableEventError> {
info!("Sending event to frontend: {}", Self::FRONTEND_EVENT_NAME);
let result = app.emit(Self::FRONTEND_EVENT_NAME, self);
if let Err(err) = result {
return Err(SendableEventError::from(err));
}
Ok(())
}
}
-6
View File
@@ -1,6 +0,0 @@
pub mod commands;
pub mod events;
pub mod state;
pub mod threads;
pub mod utils;
pub mod windows;
@@ -1,73 +0,0 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME;
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
use std::fs::OpenOptions;
use std::io::Write;
use tauri::{AppHandle, Manager, PhysicalPosition, Window};
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct MainWindowPosition {
pub x: i32,
pub y: i32,
}
impl MainWindowPosition {
pub fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
pub fn from_main_window(app: &AppHandle) -> AnyhowResult<Self> {
let windows = app.windows();
windows.get(MAIN_WINDOW_NAME)
.map(|window| Self::from_window(window))
.unwrap_or_else(|| Err(anyhow::anyhow!("Main window not found")))
}
pub fn from_window(window: &Window) -> AnyhowResult<Self> {
let position = window.outer_position()?;
Ok(Self {
x: position.x,
y: position.y,
})
}
pub fn from_filesystem_configs(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
let filename = app_data_root.get_window_position_config_file();
if !filename.exists() {
return Ok(None);
}
let contents = std::fs::read_to_string(filename)?;
let pos : MainWindowPosition = serde_json::from_str(&contents)?;
Ok(Some(pos))
}
pub fn apply_to_main_window(&self, app: &AppHandle) -> AnyhowResult<()> {
let windows = app.windows();
let window = windows.get(MAIN_WINDOW_NAME)
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
window.set_position(PhysicalPosition::new(self.x, self.y))?;
Ok(())
}
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.get_window_position_config_file();
let json = serde_json::to_string(self)?;
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)?;
file.write_all(json.as_bytes())?;
file.flush()?;
Ok(())
}
pub fn to_physical_position(&self) -> PhysicalPosition<i32> {
PhysicalPosition::new(self.x, self.y)
}
pub fn matches_physical_position(&self, pos: &PhysicalPosition<i32>) -> bool {
self.x == pos.x && self.y == pos.y
}
}
@@ -1,34 +0,0 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_position::MainWindowPosition;
use crate::core::state::main_window_size::MainWindowSize;
use crate::core::utils::best_window_size_heuristic::best_window_size_heuristic;
use errors::AnyhowResult;
use log::info;
use memory_store::clone_slot::CloneSlot;
use tauri::Window;
pub async fn persist_window_position_task(
window: &Window,
app_data_root: &AppDataRoot,
window_pos_slot: &CloneSlot<MainWindowPosition>,
) -> AnyhowResult<()> {
let current_window_position = MainWindowPosition::from_window(window)?;
let current_physical_pos = window.outer_position()?;
let mut save_size_to_disk = true;
if let Ok(Some(old_size)) = window_pos_slot.get_clone() {
if old_size.matches_physical_position(&current_physical_pos) {
save_size_to_disk = false;
}
}
if save_size_to_disk {
info!("Saving window position configs to disk...");
current_window_position.persist_to_filesystem(app_data_root)?;
window_pos_slot.set_clone(&current_window_position)?;
}
Ok(())
}
@@ -1,2 +0,0 @@
pub mod discord_presence_thread;
pub mod main_window_thread;
@@ -1,25 +0,0 @@
use tauri::{PhysicalSize, Window};
use errors::AnyhowResult;
// A typical height difference when dev tools are open:
// Window inner size = PhysicalSize { width: 3704, height: 732 }
// Window outer size = PhysicalSize { width: 3704, height: 1476 }
const HEIGHT_DIFFERENCE_THRESHOLD: u32 = 300;
/// Get the best window size
pub fn best_window_size_heuristic(window: &Window) -> AnyhowResult<PhysicalSize<u32>> {
// NB: Ordinarily we'd use inner size, as the outer size includes the window decorations.
// However, dev tools contract the inner size, and in the event they're open, we should
// prefer the outer size.
let inner = window.inner_size()?;
let outer = window.outer_size()?;
let height_delta = outer.height.saturating_sub(inner.height);
if height_delta > HEIGHT_DIFFERENCE_THRESHOLD {
// Dev tools are open, use the outer size
Ok(outer)
} else {
// Dev tools are not open, use the inner size
Ok(inner)
}
}
@@ -1,29 +0,0 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile;
use errors::AnyhowResult;
use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tempfile::NamedTempFile;
use tokens::tokens::media_files::MediaFileToken;
// TODO: Better more concrete error handling
pub async fn download_media_file_to_temp_dir(app_data_root: &AppDataRoot, token: &MediaFileToken) -> AnyhowResult<NamedTempFile> {
let response = get_media_file(&ApiHost::Storyteller, token).await?;
let media_file_url = &response.media_file.media_links.cdn_url;
let extension_with_dot = get_url_file_extension(media_file_url)
.map(|ext| format!(".{}", ext))
.unwrap_or_else(|| ".png".to_string()); // TODO: Better default extension passed by caller as heuristic.
//let filename = format!("{}{}", response.media_file.token.as_str(), extension_with_dot);
//let filename = app_data_root.downloads_dir().path().join(&filename);
let mut file = app_data_root.temp_dir().new_named_temp_file_with_extension(&extension_with_dot)?;
simple_http_download_to_tempfile(&media_file_url, &mut file).await?;
Ok(file) // NB: Must return TempFile to not drop / delete it
}
@@ -1,20 +0,0 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use std::io::Write;
use tempfile::NamedTempFile;
use url::Url;
pub async fn download_url_to_temp_dir(url: &str, app_data_root: &AppDataRoot) -> AnyhowResult<NamedTempFile> {
let url = Url::parse(&url)?;
let response = reqwest::get(url.clone()).await?;
let response_bytes = response.bytes().await?;
let ext = url.path().split(".").last().unwrap_or("png");
let mut temp_file = app_data_root.temp_dir().new_named_temp_file_with_extension(ext)?;
temp_file.write_all(&response_bytes)?;
Ok(temp_file)
}
@@ -1,12 +0,0 @@
pub mod best_window_size_heuristic;
pub mod clear_all_webview_cookies;
pub mod download_media_file_to_temp_dir;
pub mod download_url_to_temp_dir;
pub mod get_url_file_extension;
pub mod image;
pub mod large_file_transfer;
pub mod object_store;
pub mod save_base64_image_to_temp_dir;
pub mod simple_http_download;
pub mod simple_http_download_to_tempfile;
pub mod webview_unsafe;
@@ -1,25 +0,0 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use errors::AnyhowResult;
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use std::io::Write;
use tempfile::NamedTempFile;
// TODO: Retain error types (io/decode)
pub async fn save_base64_image_to_temp_dir(app_data_root: &AppDataRoot, base64_image: String) -> AnyhowResult<NamedTempFile> {
let bytes = BASE64_STANDARD.decode(base64_image)?;
let extension = MimetypeInfo::get_for_bytes(&bytes)
.map(|info| info.file_extension())
.flatten()
.map(|ext| ext.extension_with_period().to_string())
.unwrap_or_else(|| ".png".to_string());
let mut file = app_data_root.temp_dir().new_named_temp_file_with_extension(&extension)?;
file.write_all(bytes.as_ref())?;
Ok(file) // NB: Must return TempFile to not drop / delete it
}
@@ -1,26 +0,0 @@
use errors::AnyhowResult;
use reqwest::Client;
use reqwest::Url;
use std::io::Write;
use tempfile::NamedTempFile;
const USER_AGENT: &str = "storyteller-client/1.0";
/// Downloads a (binary) file to a filesystem path. Good for images, etc. Not great for large files.
pub async fn simple_http_download_to_tempfile(url: &Url, temp_file: &mut NamedTempFile) -> AnyhowResult<()> {
let client = Client::builder()
.gzip(true)
.build()?;
let response = client.get(url.clone()) // NB: No IntoUrl for &Url.
.header("User-Agent", USER_AGENT)
.header("Accept", "*/*")
.send()
.await?;
let bytes = response.bytes().await?;
temp_file.write_all(&bytes)?;
Ok(())
}
@@ -1,4 +0,0 @@
/// The name of the main application window.
/// This is the Artcraft application UX itself.
pub const MAIN_WINDOW_NAME : &str = "main";
@@ -1 +0,0 @@
pub mod constants;
@@ -1 +0,0 @@
pub mod main_window;
@@ -0,0 +1 @@
pub mod notification_event;
+27 -72
View File
@@ -1,39 +1,32 @@
pub mod core;
pub mod services;
pub mod commands;
pub mod events;
pub mod state;
pub mod threads;
pub mod utils;
use tauri::Manager;
use crate::core::commands::app_preferences::get_app_preferences_command::get_app_preferences_command;
use crate::core::commands::app_preferences::update_app_preference_command::update_app_preferences_command;
use crate::core::commands::flip_image::flip_image;
use crate::core::commands::platform_info_command::platform_info_command;
use crate::core::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_position::MainWindowPosition;
use crate::core::state::main_window_size::MainWindowSize;
use crate::core::threads::discord_presence_thread::discord_presence_thread;
use crate::core::threads::main_window_thread::main_window_thread::main_window_thread;
use crate::core::utils::webview_unsafe::webview_unsafe_for_app;
use crate::services::fal::commands::fal_background_removal_command::fal_background_removal_command;
use crate::services::fal::commands::fal_hunyuan_image_to_3d_command::fal_hunyuan_image_to_3d_command;
use crate::services::fal::commands::fal_kling_image_to_video_command::fal_kling_image_to_video_command;
use crate::services::fal::commands::get_fal_api_key_command::get_fal_api_key_command;
use crate::services::fal::commands::set_fal_api_key_command::set_fal_api_key_command;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::fal::threads::fal_task_polling_thread::fal_task_polling_thread;
use crate::services::sora::commands::check_sora_session_command::check_sora_session_command;
use crate::services::sora::commands::open_sora_login_command::open_sora_login_command;
use crate::services::sora::commands::sora_image_generation_command::sora_image_generation_command;
use crate::services::sora::commands::sora_image_remix_command::sora_image_remix_command;
use crate::services::sora::commands::sora_logout_command::sora_logout_command;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::sora::threads::sora_task_polling_thread::sora_task_polling_thread;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use crate::commands::flip_image::flip_image;
use crate::commands::app_preferences::get_app_preferences_command::get_app_preferences_command;
use crate::commands::app_preferences::update_app_preference_command::update_app_preferences_command;
use crate::commands::platform_info_command::platform_info_command;
use crate::commands::sora::open_sora_login_command::open_sora_login_command;
use crate::commands::sora::sora_image_generation_command::sora_image_generation_command;
use crate::commands::sora::sora_image_remix_command::sora_image_remix_command;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_size::MainWindowSize;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::state::sora::sora_task_queue::SoraTaskQueue;
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
use crate::threads::discord_presence_thread::discord_presence_thread;
use crate::threads::main_window_thread::main_window_thread::main_window_thread;
use crate::threads::sora_session_login_thread::sora_session_login_thread;
use crate::threads::sora_task_polling_thread::sora_task_polling_thread;
use crate::utils::webview_unsafe::webview_unsafe_for_app;
use tauri_plugin_dialog;
use crate::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
use tauri_plugin_http;
use tauri_plugin_dialog;
use tauri_plugin_log::Target;
use tauri_plugin_log::TargetKind;
@@ -51,23 +44,15 @@ pub fn run() {
println!("Attempting to read existing artcraft credentials...");
let storyteller_creds_manager = StorytellerCredentialManager::initialize_from_disk_infallible(&app_data_root);
let storyteller_creds_manager_2 = storyteller_creds_manager.clone();
let storyteller_creds_manager_3 = storyteller_creds_manager.clone();
println!("Attempting to read existing sora credentials...");
println!("Attempting to read existing credentials...");
let sora_creds_manager = SoraCredentialManager::initialize_from_disk_infallible(&app_data_root);
let sora_creds_manager_2 = sora_creds_manager.clone();
println!("Attempting to read existing fal credentials...");
let fal_creds_manager = FalCredentialManager::initialize_from_disk_infallible(&app_data_root);
let fal_creds_manager_2 = fal_creds_manager.clone();
// Other state
let sora_task_queue = SoraTaskQueue::new();
let sora_task_queue_2 = sora_task_queue.clone();
let fal_task_queue = FalTaskQueue::new();
let fal_task_queue_2 = fal_task_queue.clone();
println!("Initializing backend runtime...");
tauri::Builder::default()
@@ -101,19 +86,13 @@ pub fn run() {
eprintln!("Error setting webview unsafe: {:?}", err);
}
// TODO(bt): Clean this up. We can just clone at the callsite. Also clean initialization
let app_2 = app.clone();
let app_3 = app.clone();
let app_4 = app.clone();
let app_5 = app.clone();
let app_data_root_3 = app_data_root_2.clone();
let app_data_root_4 = app_data_root_2.clone();
let app_data_root_5 = app_data_root_2.clone();
let sora_creds_manager_3 = sora_creds_manager_2.clone();
let storyteller_creds_manager_3 = storyteller_creds_manager.clone();
let storyteller_creds_manager_4 = storyteller_creds_manager.clone();
let fal_creds_manager_3 = fal_creds_manager_2.clone();
let fal_task_queue_3 = fal_task_queue_2.clone();
match MainWindowSize::from_filesystem_configs(&app_data_root_3) {
Ok(None) => {}
@@ -129,49 +108,25 @@ pub fn run() {
}
}
match MainWindowPosition::from_filesystem_configs(&app_data_root_3) {
Ok(None) => {}
Ok(Some(pos)) => {
println!("Moving window to: {:?}", pos);
let result = pos.apply_to_main_window(&app);
if let Err(err) = result {
eprintln!("Could not set window position: {:?}", err);
}
}
Err(err) => {
eprintln!("Failed to read window position from disk: {:?}", err);
}
}
tauri::async_runtime::spawn(sora_session_login_thread(app_2, app_data_root_2, sora_creds_manager_2));
tauri::async_runtime::spawn(main_window_thread(app_3, app_data_root_3, storyteller_creds_manager_2));
tauri::async_runtime::spawn(sora_task_polling_thread(app_4, app_data_root_4, sora_creds_manager_3, storyteller_creds_manager_3, sora_task_queue_2));
tauri::async_runtime::spawn(discord_presence_thread());
tauri::async_runtime::spawn(fal_task_polling_thread(app_5, app_data_root_5, fal_creds_manager_3, storyteller_creds_manager_4, fal_task_queue_3));
Ok(())
})
.manage(app_data_root)
.manage(app_preferences)
.manage(fal_creds_manager)
.manage(fal_task_queue)
.manage(sora_creds_manager)
.manage(sora_task_queue)
.manage(storyteller_creds_manager_3)
.invoke_handler(tauri::generate_handler![
check_sora_session_command,
fal_background_removal_command,
fal_hunyuan_image_to_3d_command,
fal_kling_image_to_video_command,
flip_image,
get_app_preferences_command,
get_fal_api_key_command,
update_app_preferences_command,
open_sora_login_command,
platform_info_command,
set_fal_api_key_command,
sora_image_generation_command,
sora_image_remix_command,
sora_logout_command,
update_app_preferences_command,
])
.run(tauri::generate_context!("tauri.conf.json"))
.expect("error while running tauri application");
@@ -1,257 +0,0 @@
use crate::core::commands::command_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus, CommandResult, SerializeMarker};
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
use crate::core::utils::simple_http_download::simple_http_download;
use crate::core::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::sora::events::sora_image_enqueue_failure_event::SoraImageEnqueueFailureEvent;
use crate::services::sora::events::sora_image_enqueue_success_event::SoraImageEnqueueSuccessEvent;
use crate::services::sora::state::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::services::sora::state::sora_credential_holder::SoraCredentialHolder;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use base64::prelude::BASE64_STANDARD;
use base64::{DecodeError, Engine};
use errors::{AnyhowError, AnyhowResult};
use fal_client::fal_error_plus::FalErrorPlus;
use fal_client::requests::remove_background_rembg::remove_background_rembg;
use filesys::file_read_bytes::file_read_bytes;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{DynamicImage, EncodableLayout, ImageReader};
use log::{debug, error, info, warn};
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::creds::credential_migration::CredentialMigrationRef;
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use openai_sora_client::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file;
use openai_sora_client::sora_error::SoraError;
use reqwest::Url;
use serde_derive::{Deserialize, Serialize};
use std::fs::read_to_string;
use std::io::{Cursor, Read, Write};
use std::ops::Add;
use std::path::PathBuf;
use std::time::Duration;
use storyteller_client::error::api_error::ApiError;
use storyteller_client::error::api_error::ApiError::InternalServerError;
use storyteller_client::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse};
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tempfile::NamedTempFile;
use tokens::tokens::media_files::MediaFileToken;
#[derive(Deserialize)]
pub struct FalBackgroundRemovalRequest {
/// Image media file; the image to remove the background from.
pub image_media_token: Option<MediaFileToken>,
/// Base64-encoded image
pub base64_image: Option<String>,
}
#[derive(Serialize)]
pub struct FalBackgroundRemovalSuccessResponse {
/// Result media token
pub media_token: MediaFileToken,
/// Result URL
pub cdn_url: Url,
/// Base64 bytes
pub base64_bytes: String,
}
impl SerializeMarker for FalBackgroundRemovalSuccessResponse {}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum BackgroundRemovalErrorType {
/// Generic server error
ServerError,
/// No Fal API key available
NeedsFalApiKey,
}
#[tauri::command]
pub async fn fal_background_removal_command(
request: FalBackgroundRemovalRequest,
app_data_root: State<'_, AppDataRoot>,
fal_creds_manager: State<'_, FalCredentialManager>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
) -> CommandResult<FalBackgroundRemovalSuccessResponse, BackgroundRemovalErrorType, ()> {
info!("fal_background_removal_command called; image media token: {:?}", request.image_media_token);
let has_credentials = fal_creds_manager
.has_apparent_api_token()
.unwrap_or(true); // NB: Failures would be lock issues
if !has_credentials {
warn!("No API key found");
return Err(CommandErrorResponseWrapper {
status: CommandErrorStatus::Unauthorized,
error_message: Some("You need to set a FAL api key".to_string()),
error_type: Some(BackgroundRemovalErrorType::NeedsFalApiKey),
error_details: None,
});
}
let result = remove_background(
request,
&app_data_root,
&fal_creds_manager,
&storyteller_creds_manager
).await;
match result {
Err(err) => {
error!("error: {:?}", err);
//let event = SoraImageEnqueueFailureEvent {};
//let result = event.send(&app);
//if let Err(err) = result {
// error!("Failed to emit event: {:?}", err);
//}
let mut status = CommandErrorStatus::ServerError;
let mut error_type = BackgroundRemovalErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
//match (err) {
// _ => {},
//}
Err(CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
})
}
Ok(result) => {
//let event = SoraImageEnqueueSuccessEvent {};
//let result = event.send(&app);
//if let Err(err) = result {
// error!("Failed to emit event: {:?}", err);
//}
Ok(FalBackgroundRemovalSuccessResponse {
media_token: result.0.media_file.token,
cdn_url: result.0.media_file.media_links.cdn_url,
base64_bytes: result.1,
}.into())
}
}
}
#[derive(Debug)]
enum InnerError {
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerApiError(ApiError),
DecodeError(DecodeError),
IoError(std::io::Error),
}
impl From<AnyhowError> for InnerError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<FalErrorPlus> for InnerError {
fn from(value: FalErrorPlus) -> Self {
Self::FalError(value)
}
}
impl From<ApiError> for InnerError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
}
}
impl From<DecodeError> for InnerError {
fn from(value: DecodeError) -> Self {
Self::DecodeError(value)
}
}
impl From<std::io::Error> for InnerError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
pub async fn remove_background(
request: FalBackgroundRemovalRequest,
app_data_root: &AppDataRoot,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<(GetMediaFileSuccessResponse, String), InnerError> {
let api_key = fal_creds_manager.get_key_required()?;
let creds = storyteller_creds_manager.get_credentials_required()?;
let mut temp_download;
if let Some(media_token) = request.image_media_token {
temp_download = download_media_file_to_temp_dir(&app_data_root, &media_token).await?;
} else if let Some(base64_bytes) = request.base64_image {
temp_download = save_base64_image_to_temp_dir(&app_data_root, base64_bytes).await?;
} else {
return Err(InnerError::AnyhowError(anyhow!("No image media token or base64 image provided")));
}
info!("Calling FAL image background removal...");
let filename = temp_download.path().to_path_buf();
let result = remove_background_rembg(filename, &api_key).await?;
let extension_with_dot = get_url_file_extension(&result.image_url)
.map(|ext| format!(".{}", ext))
.unwrap_or_else(|| ".png".to_string());
let mut result_file = app_data_root.temp_dir().new_named_temp_file_with_extension(&extension_with_dot)?;
info!("Downloading file result file...");
simple_http_download_to_tempfile(&result.image_url, &mut result_file).await?;
let result_filename = result_file.path().to_path_buf();
// NB: We couldn't pass the existing file handle as I think the file handle pointer is already at the EOF
let bytes = file_read_bytes(&result_filename)?;
let base64_bytes = BASE64_STANDARD.encode(bytes.as_bytes());
info!("Uploading image media file...");
let upload_result = upload_image_media_file_from_file(
&ApiHost::Storyteller,
Some(&creds),
result_filename
).await?;
// TODO: Don't re-request to simply build MediaLinks (or CDN URL). Get those from the upload API in one turn.
info!("Re-requesting media file...");
let response = get_media_file(&ApiHost::Storyteller, &upload_result.media_file_token).await?;
info!("Uploaded media file: {:?}", response.media_file.token);
Ok((response, base64_bytes))
}
@@ -1,220 +0,0 @@
use crate::core::commands::command_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus, CommandResult, SerializeMarker};
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
use crate::core::utils::simple_http_download::simple_http_download;
use crate::core::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::events::sora_image_enqueue_failure_event::SoraImageEnqueueFailureEvent;
use crate::services::sora::events::sora_image_enqueue_success_event::SoraImageEnqueueSuccessEvent;
use crate::services::sora::state::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::services::sora::state::sora_credential_holder::SoraCredentialHolder;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use base64::prelude::BASE64_STANDARD;
use base64::{DecodeError, Engine};
use errors::{AnyhowError, AnyhowResult};
use fal_client::fal_error_plus::FalErrorPlus;
use fal_client::requests::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
use fal_client::requests::enqueue_kling_16_image_to_video::{enqueue_kling_16_image_to_video, Kling16Args, Kling16Duration};
use fal_client::requests::remove_background_rembg::remove_background_rembg;
use filesys::file_read_bytes::file_read_bytes;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{DynamicImage, EncodableLayout, ImageReader};
use log::{debug, error, info, warn};
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::creds::credential_migration::CredentialMigrationRef;
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use openai_sora_client::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file;
use openai_sora_client::sora_error::SoraError;
use reqwest::Url;
use serde_derive::{Deserialize, Serialize};
use std::fs::read_to_string;
use std::io::{Cursor, Read, Write};
use std::ops::Add;
use std::path::PathBuf;
use std::time::Duration;
use storyteller_client::error::api_error::ApiError;
use storyteller_client::error::api_error::ApiError::InternalServerError;
use storyteller_client::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse};
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tempfile::NamedTempFile;
use tokens::tokens::media_files::MediaFileToken;
#[derive(Deserialize)]
pub struct FalHunyuanImageTo3dRequest {
/// Image media file; the image to remove the background from.
pub image_media_token: Option<MediaFileToken>,
/// Base64-encoded image
pub base64_image: Option<String>,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum HunyuanErrorType {
/// Generic server error
ServerError,
/// No Fal API key available
NeedsFalApiKey,
}
#[tauri::command]
pub async fn fal_hunyuan_image_to_3d_command(
request: FalHunyuanImageTo3dRequest,
app_data_root: State<'_, AppDataRoot>,
fal_creds_manager: State<'_, FalCredentialManager>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
fal_task_queue: State<'_, FalTaskQueue>,
) -> CommandResult<(), HunyuanErrorType, ()> {
info!("fal_hunyuan_image_to_3d_command called; image media token: {:?}", request.image_media_token);
let has_credentials = fal_creds_manager
.has_apparent_api_token()
.unwrap_or(true); // NB: Failures would be lock issues
if !has_credentials {
warn!("No API key found");
return Err(CommandErrorResponseWrapper {
status: CommandErrorStatus::Unauthorized,
error_message: Some("You need to set a FAL api key".to_string()),
error_type: Some(HunyuanErrorType::NeedsFalApiKey),
error_details: None,
});
}
let result = image_to_3d(
request,
&app_data_root,
&fal_creds_manager,
&storyteller_creds_manager,
&fal_task_queue,
).await;
if let Err(err) = result {
error!("error: {:?}", err);
//let event = SoraImageEnqueueFailureEvent {};
//let result = event.send(&app);
//if let Err(err) = result {
// error!("Failed to emit event: {:?}", err);
//}
let mut status = CommandErrorStatus::ServerError;
let mut error_type = HunyuanErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
//match (err) {
// _ => {},
//}
return Err(CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
})
}
//let event = SoraImageEnqueueSuccessEvent {};
//let result = event.send(&app);
//if let Err(err) = result {
// error!("Failed to emit event: {:?}", err);
//}
Ok(().into())
}
#[derive(Debug)]
enum InnerError {
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerApiError(ApiError),
DecodeError(DecodeError),
IoError(std::io::Error),
}
impl From<AnyhowError> for InnerError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<FalErrorPlus> for InnerError {
fn from(value: FalErrorPlus) -> Self {
Self::FalError(value)
}
}
impl From<ApiError> for InnerError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
}
}
impl From<DecodeError> for InnerError {
fn from(value: DecodeError) -> Self {
Self::DecodeError(value)
}
}
impl From<std::io::Error> for InnerError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
pub async fn image_to_3d(
request: FalHunyuanImageTo3dRequest,
app_data_root: &AppDataRoot,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InnerError> {
let api_key = fal_creds_manager.get_key_required()?;
let creds = storyteller_creds_manager.get_credentials_required()?;
let mut temp_download;
if let Some(media_token) = request.image_media_token {
temp_download = download_media_file_to_temp_dir(&app_data_root, &media_token).await?;
} else if let Some(base64_bytes) = request.base64_image {
temp_download = save_base64_image_to_temp_dir(&app_data_root, base64_bytes).await?;
} else {
return Err(InnerError::AnyhowError(anyhow!("No image media token or base64 image provided")));
}
info!("Calling FAL image to 3d ...");
let filename = temp_download.path().to_path_buf();
let enqueued = enqueue_hunyuan2_image_to_3d(Hunyuan2Args {
image_path: filename,
api_key: &api_key,
}).await?;
if let Err(err) = fal_task_queue.insert(&enqueued) {
error!("Failed to enqueue task: {:?}", err);
return Err(InnerError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
}
Ok(())
}
@@ -1,220 +0,0 @@
use crate::core::commands::command_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus, CommandResult, SerializeMarker};
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
use crate::core::utils::simple_http_download::simple_http_download;
use crate::core::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::events::sora_image_enqueue_failure_event::SoraImageEnqueueFailureEvent;
use crate::services::sora::events::sora_image_enqueue_success_event::SoraImageEnqueueSuccessEvent;
use crate::services::sora::state::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::services::sora::state::sora_credential_holder::SoraCredentialHolder;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use base64::prelude::BASE64_STANDARD;
use base64::{DecodeError, Engine};
use errors::{AnyhowError, AnyhowResult};
use fal_client::fal_error_plus::FalErrorPlus;
use fal_client::requests::enqueue_kling_16_image_to_video::{enqueue_kling_16_image_to_video, Kling16Args, Kling16Duration};
use fal_client::requests::remove_background_rembg::remove_background_rembg;
use filesys::file_read_bytes::file_read_bytes;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{DynamicImage, EncodableLayout, ImageReader};
use log::{debug, error, info, warn};
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::creds::credential_migration::CredentialMigrationRef;
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use openai_sora_client::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file;
use openai_sora_client::sora_error::SoraError;
use reqwest::Url;
use serde_derive::{Deserialize, Serialize};
use std::fs::read_to_string;
use std::io::{Cursor, Read, Write};
use std::ops::Add;
use std::path::PathBuf;
use std::time::Duration;
use storyteller_client::error::api_error::ApiError;
use storyteller_client::error::api_error::ApiError::InternalServerError;
use storyteller_client::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse};
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tempfile::NamedTempFile;
use tokens::tokens::media_files::MediaFileToken;
#[derive(Deserialize)]
pub struct FalKlingImageToVideoRequest {
/// Image media file; the image to remove the background from.
pub image_media_token: Option<MediaFileToken>,
/// Base64-encoded image
pub base64_image: Option<String>,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum KlingImageToVideoErrorType {
/// Generic server error
ServerError,
/// No Fal API key available
NeedsFalApiKey,
}
#[tauri::command]
pub async fn fal_kling_image_to_video_command(
request: FalKlingImageToVideoRequest,
app_data_root: State<'_, AppDataRoot>,
fal_creds_manager: State<'_, FalCredentialManager>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
fal_task_queue: State<'_, FalTaskQueue>,
) -> CommandResult<(), KlingImageToVideoErrorType, ()> {
info!("fal_kling_image_to_video_command called; image media token: {:?}", request.image_media_token);
let has_credentials = fal_creds_manager
.has_apparent_api_token()
.unwrap_or(true); // NB: Failures would be lock issues
if !has_credentials {
warn!("No API key found");
return Err(CommandErrorResponseWrapper {
status: CommandErrorStatus::Unauthorized,
error_message: Some("You need to set a FAL api key".to_string()),
error_type: Some(KlingImageToVideoErrorType::NeedsFalApiKey),
error_details: None,
});
}
let result = image_to_video(
request,
&app_data_root,
&fal_creds_manager,
&storyteller_creds_manager,
&fal_task_queue,
).await;
if let Err(err) = result {
error!("error: {:?}", err);
//let event = SoraImageEnqueueFailureEvent {};
//let result = event.send(&app);
//if let Err(err) = result {
// error!("Failed to emit event: {:?}", err);
//}
let mut status = CommandErrorStatus::ServerError;
let mut error_type = KlingImageToVideoErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
//match (err) {
// _ => {},
//}
return Err(CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
})
}
//let event = SoraImageEnqueueSuccessEvent {};
//let result = event.send(&app);
//if let Err(err) = result {
// error!("Failed to emit event: {:?}", err);
//}
Ok(().into())
}
#[derive(Debug)]
enum InnerError {
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerApiError(ApiError),
DecodeError(DecodeError),
IoError(std::io::Error),
}
impl From<AnyhowError> for InnerError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<FalErrorPlus> for InnerError {
fn from(value: FalErrorPlus) -> Self {
Self::FalError(value)
}
}
impl From<ApiError> for InnerError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
}
}
impl From<DecodeError> for InnerError {
fn from(value: DecodeError) -> Self {
Self::DecodeError(value)
}
}
impl From<std::io::Error> for InnerError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
pub async fn image_to_video(
request: FalKlingImageToVideoRequest,
app_data_root: &AppDataRoot,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InnerError> {
let api_key = fal_creds_manager.get_key_required()?;
let creds = storyteller_creds_manager.get_credentials_required()?;
let mut temp_download;
if let Some(media_token) = request.image_media_token {
temp_download = download_media_file_to_temp_dir(&app_data_root, &media_token).await?;
} else if let Some(base64_bytes) = request.base64_image {
temp_download = save_base64_image_to_temp_dir(&app_data_root, base64_bytes).await?;
} else {
return Err(InnerError::AnyhowError(anyhow!("No image media token or base64 image provided")));
}
info!("Calling FAL image to video ...");
let filename = temp_download.path().to_path_buf();
let enqueued = enqueue_kling_16_image_to_video(Kling16Args {
image_path: filename,
api_key: &api_key,
duration: Kling16Duration::Default,
}).await?;
if let Err(err) = fal_task_queue.insert(&enqueued) {
error!("Failed to enqueue task: {:?}", err);
return Err(InnerError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
}
Ok(())
}
@@ -1,46 +0,0 @@
use crate::core::commands::command_response_wrapper::{CommandResult, SerializeMarker};
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use fal_client::creds::fal_api_key::FalApiKey;
use log::{error, info};
use openai_sora_client::sora_error::SoraError::AnyhowError;
use primitives::traits::trim_or_emptyable::TrimOrEmptyable;
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
#[derive(Serialize)]
pub struct GetFalApiKeyResponse {
pub key: Option<String>,
}
impl SerializeMarker for GetFalApiKeyResponse {}
#[tauri::command]
pub async fn get_fal_api_key_command(
creds_manager: State<'_, FalCredentialManager>,
) -> CommandResult<GetFalApiKeyResponse, (), ()> {
let key = get_key(&creds_manager)
.await
.map_err(|err| {
error!("Error getting API key: {:?}", err);
"error getting key"
})?;
Ok(key.into())
}
async fn get_key(
creds_manager: &FalCredentialManager,
) -> AnyhowResult<GetFalApiKeyResponse> {
let maybe_key = creds_manager.get_key()?
.map(|key| key.0)
.map(|key| key.trim().to_string())
.filter(|key| !key.is_empty());
Ok(GetFalApiKeyResponse { key: maybe_key })
}
@@ -1,5 +0,0 @@
pub mod fal_background_removal_command;
pub mod fal_hunyuan_image_to_3d_command;
pub mod fal_kling_image_to_video_command;
pub mod get_fal_api_key_command;
pub mod set_fal_api_key_command;
@@ -1,59 +0,0 @@
use crate::core::commands::command_response_wrapper::CommandResult;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use fal_client::creds::fal_api_key::FalApiKey;
use log::{error, info};
use openai_sora_client::sora_error::SoraError::AnyhowError;
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
/// For now, we'll only update a single preference at a time.
#[derive(Deserialize)]
pub struct SetFalApiKeyRequest{
pub key: Option<String>,
}
#[tauri::command]
pub async fn set_fal_api_key_command(
request: SetFalApiKeyRequest,
creds_manager: State<'_, FalCredentialManager>,
) -> CommandResult<(), (), ()> {
info!("update_app_preferences_command called");
set_key(request, &creds_manager)
.await
.map_err(|err| {
error!("Error getting app preferences: {:?}", err);
"error setting key"
})?;
Ok(().into())
}
async fn set_key(
request: SetFalApiKeyRequest,
creds_manager: &FalCredentialManager,
) -> AnyhowResult<()> {
let key = request.key
.map(|key| key.trim().to_string())
.filter(|key| !key.is_empty())
.map(|key| FalApiKey::from_str(&key));
match key {
None => {
creds_manager.clear_key()?;
creds_manager.purge_api_key_from_disk()?;
}
Some(key) => {
creds_manager.set_key(&key)?;
creds_manager.persist_to_disk()?;
}
}
Ok(())
}
@@ -1,3 +0,0 @@
pub mod commands;
pub mod state;
pub mod threads;
@@ -1,98 +0,0 @@
use crate::services::fal::state::read_fal_api_key_from_disk::read_fal_api_key_from_disk;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use fal_client::creds::fal_api_key::FalApiKey;
use memory_store::clone_slot::CloneSlot;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use std::{fs, io};
#[derive(Clone)]
pub struct FalCredentialManager {
key: CloneSlot<FalApiKey>,
app_data_root: AppDataRoot,
}
impl FalCredentialManager {
pub fn new(app_data_root: &AppDataRoot) -> Self {
Self {
key: CloneSlot::empty(),
app_data_root: app_data_root.clone(),
}
}
pub fn initialize_from_disk_infallible(app_data_root: &AppDataRoot) -> Self {
let mut holder = CloneSlot::empty();
match read_fal_api_key_from_disk(app_data_root) {
Err(err) => {
log::warn!("Failed to read FAL API key from disk: {:?}", err);
}
Ok(api_key) => {
holder.set_clone(&api_key).expect("Failed to set FAL API key");
}
}
Self {
key: holder,
app_data_root: app_data_root.clone(),
}
}
pub fn set_key(&self, key: &FalApiKey) -> AnyhowResult<()> {
self.key.set_clone(key)
}
pub fn get_key(&self) -> AnyhowResult<Option<FalApiKey>> {
self.key.get_clone()
}
pub fn get_key_required(&self) -> AnyhowResult<FalApiKey> {
self.key.get_clone_required()
}
pub fn clear_key(&self) -> AnyhowResult<()> {
self.key.clear()
}
/// Does not check the token for validity
pub fn has_apparent_api_token(&self) -> AnyhowResult<bool> {
self.key.is_some()
}
pub fn purge_api_key_from_disk(&self) -> Result<(), io::Error> {
let filename = self.app_data_root.credentials_dir().get_fal_api_key_file_path();
if filename.exists() && filename.is_file() {
fs::remove_file(filename)?;
}
Ok(())
}
pub fn persist_to_disk(&self) -> AnyhowResult<()> {
let key = match self.get_key() {
Ok(Some(key)) => key,
Ok(None) => {
self.purge_api_key_from_disk()?;
return Ok(());
},
Err(err) => {
return Err(anyhow!("Failed to get FAL key: {:?}", err));
}
};
let filename = self.app_data_root.credentials_dir().get_fal_api_key_file_path();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)?;
file.write_all(key.0.as_bytes())?;
file.flush()?;
Ok(())
}
}
@@ -1,164 +0,0 @@
use chrono::{DateTime, Utc};
use errors::AnyhowResult;
use fal_client::model::enqueued_request::EnqueuedRequest;
use fal_client::model::fal_request_id::FalRequestId;
use indexmap::IndexMap;
use std::collections::HashSet;
use std::sync::{Arc, RwLock};
#[derive(Clone)]
pub struct FalTaskDetails {
pub enqueue_time: DateTime<Utc>,
pub enqueued_request: EnqueuedRequest,
}
impl FalTaskDetails {
pub fn for_request(request: EnqueuedRequest) -> Self {
Self {
enqueue_time: Utc::now(),
enqueued_request: request,
}
}
}
#[derive(Clone)]
pub struct FalTaskQueue {
// Insertion-order map of task IDs to task details
queue: Arc<RwLock<IndexMap<FalRequestId, FalTaskDetails>>>
}
impl FalTaskQueue {
pub fn new() -> Self {
Self {
queue: Arc::new(RwLock::new(IndexMap::new()))
}
}
/// Determine if queue is empty
/// This is an O(1) operation.
pub fn is_empty(&self) -> AnyhowResult<bool> {
match self.queue.read() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
}
Ok(lock) => Ok(lock.is_empty())
}
}
/// Get length of set
/// This is an O(1) operation.
pub fn len(&self) -> AnyhowResult<usize> {
match self.queue.read() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
}
Ok(lock) => Ok(lock.len())
}
}
/// Get the task at an index.
/// This is an O(1) operation.
pub fn get_index(&self, i: usize) -> AnyhowResult<Option<FalTaskDetails>> {
match self.queue.read() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
}
Ok(lock) => {
if i >= lock.len() {
Ok(None)
} else {
match lock.get_index(i) {
None => Ok(None),
Some((_key, value)) => {
Ok(Some(value.clone()))
}
}
}
}
}
}
/// Determine if the key is in the queue
/// This is an O(1) operation.
pub fn contains_key(&self, request_id: &FalRequestId) -> AnyhowResult<bool> {
match self.queue.read() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
}
Ok(lock) => Ok(lock.contains_key(request_id))
}
}
/// Get the first enqueued task
/// This is an O(1) operation.
pub fn first(&self) -> AnyhowResult<Option<FalTaskDetails>> {
match self.queue.read() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
}
Ok(lock) => match lock.first() {
None => Ok(None),
Some((request_id, task_details)) => Ok(Some(task_details.clone()))
}
}
}
/// Get the last enqueued task
/// This is an O(1) operation.
pub fn last(&self) -> AnyhowResult<Option<FalTaskDetails>> {
match self.queue.read() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
}
Ok(lock) => match lock.last() {
None => Ok(None),
Some((request_id, task_details)) => Ok(Some(task_details.clone()))
}
}
}
/// Insert a new task. If a task already exists, we update its value.
/// This is an O(1) operation.
pub fn insert(&self, request: &EnqueuedRequest) -> AnyhowResult<Option<FalTaskDetails>> {
match self.queue.write() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err))
}
Ok(mut lock) => {
// NB: Probably fine to compute TaskDetails here for now. We may eventually expose this if we include additional info.
let task_details = FalTaskDetails::for_request(request.clone());
Ok(lock.insert(request.request_id.clone(), task_details))
}
}
}
/// Remove a task by ID.
/// This is an O(n) operation.
pub fn remove(&self, request_id: &FalRequestId) -> AnyhowResult<Option<FalTaskDetails>> {
match self.queue.write() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err))
}
Ok(mut lock) => {
Ok(lock.shift_remove(request_id))
}
}
}
/// Remove a list of tasks by ID.
/// This is an O(n) operation.
pub fn remove_list(&self, request_ids: &[&FalRequestId]) -> AnyhowResult<()> {
match self.queue.write() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err))
}
Ok(mut lock) => {
// TODO: Cleaner way to do this?
let mut set : HashSet<&FalRequestId, _> = HashSet::with_capacity(request_ids.len());
set.extend(request_ids);
lock.retain(|k, v| !set.contains(k));
Ok(())
}
}
}
}
@@ -1,3 +0,0 @@
pub mod fal_credential_manager;
pub mod fal_task_queue;
pub mod read_fal_api_key_from_disk;
@@ -1,19 +0,0 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use fal_client::creds::fal_api_key::FalApiKey;
use std::fs::read_to_string;
pub fn read_fal_api_key_from_disk(app_data_root: &AppDataRoot) -> AnyhowResult<FalApiKey> {
let api_key_file = app_data_root.credentials_dir().get_fal_api_key_file_path();
if !api_key_file.exists() {
return Err(anyhow!("API key file does not exist: {:?}", api_key_file));
}
let value = read_to_string(api_key_file)?
.trim()
.to_string();
Ok(FalApiKey::new(value))
}
@@ -1,234 +0,0 @@
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::utils::download_url_to_temp_dir::download_url_to_temp_dir;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::events::sora_image_generation_complete_event::SoraImageGenerationCompleteEvent;
use crate::services::sora::events::sora_image_generation_failed_event::SoraImageGenerationFailedEvent;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use errors::AnyhowResult;
use fal_client::export::queue::Status;
use fal_client::fal_error_plus::FalErrorPlus;
use fal_client::model::fal_request_id::FalRequestId;
use fal_client::utils::queue_status_checker::QueueStatusChecker;
use log::{error, info, warn};
use mimetypes::mimetype_for_file::get_mimetype_for_file;
use mimetypes::mimetype_info::file_extension::FileExtension;
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use openai_sora_client::requests::image_gen::image_gen_status::{Generation, TaskId, TaskStatus};
use serde_derive::Serialize;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use storyteller_client::error::api_error::ApiError;
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use storyteller_client::media_files::upload_new_engine_asset_from_file::upload_new_engine_asset_from_file;
use storyteller_client::media_files::upload_video_media_file_from_file::{upload_video_media_file_from_file, UploadVideoMediaFileSuccessResponse};
use storyteller_client::recipes::upload_media_file_from_file::upload_media_file_from_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter};
use tempdir::TempDir;
use tempfile::NamedTempFile;
use tokens::tokens::media_files::MediaFileToken;
use url::Url;
pub async fn fal_task_polling_thread(
app_handle: AppHandle,
app_data_root: AppDataRoot,
fal_creds_manager: FalCredentialManager,
storyteller_creds_manager: StorytellerCredentialManager,
fal_task_queue: FalTaskQueue,
) -> ! {
loop {
let res = polling_loop(
&app_handle,
&fal_creds_manager,
&storyteller_creds_manager,
&fal_task_queue,
&app_data_root,
).await;
if let Err(err) = res {
error!("An error occurred: {:?}", err);
}
tokio::time::sleep(std::time::Duration::from_millis(30_000)).await;
}
}
async fn polling_loop(
app_handle: &AppHandle,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
app_data_root: &AppDataRoot,
) -> AnyhowResult<()> {
loop {
if fal_task_queue.is_empty()? {
// No need to poll if we don't have pending tasks.
tokio::time::sleep(std::time::Duration::from_millis(1_000)).await;
continue;
}
info!("Task queue has {} pending tasks.", fal_task_queue.len()?);
let key = fal_creds_manager.get_key_required()?;
let storyteller_creds = storyteller_creds_manager.get_credentials_required()?;
let queue_status_checker = QueueStatusChecker::new(key);
let mut succeeded_tasks = Vec::new();
//let mut failed_tasks = Vec::new(); // TODO
let len = fal_task_queue.len()?;
for i in 0..len {
let task = match fal_task_queue.get_index(i)? {
Some(task) => task,
None => {
warn!("Failed to get task ID at index {}", i);
break;
}
};
let task_id = task.enqueued_request.request_id.clone();
info!("Checking task status for task ID: {:?}", &task_id);
let result = queue_status_checker.check_status(&task.enqueued_request).await;
let url = match result {
Err(err) => {
warn!("Error checking job status: {:?}", err);
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
// TODO: Some failures will be permanent job failures and the jobs will need to be removed.
continue;
}
Ok(status) => {
info!("Status: {:?}", status);
match status.status {
Status::Completed => {
let result = queue_status_checker.get_download_url(&task.enqueued_request).await;
let url = match result {
Ok(url) => url,
Err(err) => {
warn!("Failed to get download url: {:?}", err);
// TODO: Some failures will be permanent job failures.
continue;
}
};
url
}
_ => {
continue;
},
}
}
};
// TODO: Clean up
let download_file = download_url_to_temp_dir(&url, &app_data_root).await?;
let result = upload_media_file_from_file(
&ApiHost::Storyteller,
Some(&storyteller_creds),
download_file
).await;
match result {
Ok(success) => {
info!("Uploaded to API backend: {:?}", success.media_file_token);
//let event = SoraImageGenerationCompleteEvent {
// media_file_token: success.media_file_token,
//};
//event.send(&app_handle)?;
succeeded_tasks.push(task_id);
}
Err(err) => {
warn!("Failed to upload media file: {:?}", err);
continue;
}
}
}
// // TODO: The cursoring logic likely needs to improve.
// for task in response.task_responses {
// let status = TaskStatus::from_str(&task.status);
//
// match status {
// TaskStatus::Succeeded => {
// succeeded_tasks.push(task);
// }
// TaskStatus::Failed => {
// failed_tasks.push(task);
// }
// TaskStatus::Queued => {}
// TaskStatus::Running => {}
// TaskStatus::Unknown(_) => {}
// }
// }
//
// for task in failed_tasks.iter() {
// if fal_task_queue.contains_key(&task.id)? {
// let event = SoraImageGenerationFailedEvent {
// prompt: task.prompt.clone(),
// };
// event.send(&app_handle)?;
// }
// }
//
// let failed_task_ids: Vec<&TaskId> = failed_tasks
// .iter()
// .map(|task| &task.id)
// .collect();
//
// fal_task_queue.remove_list(&failed_task_ids)?;
//
// let creds = storyteller_creds_manager.get_credentials_required()?;
// let api_host = ApiHost::Storyteller;
//
// for task in succeeded_tasks.iter() {
// if !fal_task_queue.contains_key(&task.id)? {
// continue;
// }
// info!("Task succeeded: {:?}", task.id);
//
// for (i, generation) in task.generations.iter().enumerate() {
// info!("Downloading generated file...");
// let download_path = download_generation(generation, &app_data_root).await?;
//
// info!("Uploading to backend...");
// let result = upload_image_media_file_from_file(&api_host, Some(&creds), download_path).await?;
//
// info!("Uploaded to API backend: {:?}", result.media_file_token);
//
// let event = SoraImageGenerationCompleteEvent {
// media_file_token: result.media_file_token,
// };
//
// event.send(&app_handle)?;
// }
// }
//
let succeeded_task_ids : Vec<&FalRequestId> = succeeded_tasks
.iter()
.map(|task| task)
.collect::<Vec<_>>();
fal_task_queue.remove_list(&succeeded_task_ids)?;
tokio::time::sleep(std::time::Duration::from_millis(6_000)).await;
}
}
@@ -1 +0,0 @@
pub mod fal_task_polling_thread;
@@ -1,3 +0,0 @@
pub mod fal;
pub mod sora;
pub mod storyteller;
@@ -1,114 +0,0 @@
use crate::core::commands::app_preferences::get_app_preferences_command::AppPreferencesPayload;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::sora::windows::sora_login_window::open_sora_login_window::open_sora_login_window;
use chrono::{DateTime, TimeDelta, Utc};
use errors::AnyhowResult;
use log::{error, info};
use once_cell::sync::Lazy;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::requests::list_media::list_media::list_media;
use serde_derive::Serialize;
use std::ops::Add;
use tauri::{AppHandle, State};
const CACHE_PERIOD : TimeDelta = TimeDelta::milliseconds(1000 * 60 * 5); // 5 minutes
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub struct CheckSoraSessionCommand {
pub state: SoraSessionState,
pub maybe_account_email: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SoraSessionState{
#[serde(rename = "not_set_up")]
NotSetUp,
#[serde(rename = "expired_or_error")]
ExpiredOrError,
#[serde(rename = "valid")]
Valid,
}
#[tauri::command]
pub async fn check_sora_session_command(
app: AppHandle,
sora_creds_manager: State<'_, SoraCredentialManager>,
) -> Result<CheckSoraSessionCommand, CheckSoraSessionCommand> {
info!("check_sora_session_command called");
match do_check(&sora_creds_manager).await {
Ok(state) => Ok(state),
Err(err) => {
error!("Error checking session state: {:?}", err);
Err(CheckSoraSessionCommand {
state: SoraSessionState::ExpiredOrError,
maybe_account_email: None,
})
}
}
}
async fn do_check(
sora_creds_manager: &SoraCredentialManager,
) -> AnyhowResult<CheckSoraSessionCommand> {
let mut creds = match sora_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Ok(CheckSoraSessionCommand {
state: SoraSessionState::NotSetUp,
maybe_account_email: None,
});
}
};
let mut needs_upgrade_or_check = false;
let stats = sora_creds_manager.get_credential_stats()?;
match stats.last_consecutive_credential_success {
None => {
info!("Session never checked, needs upgrade or status checking.");
needs_upgrade_or_check= true;
}
Some(last_success) => {
let now = chrono::Utc::now();
let expires = last_success.add(CACHE_PERIOD);
if now.gt(&expires) {
info!("Session success expired, needs upgrade or status checking.");
needs_upgrade_or_check = true;
}
}
}
let mut upgraded = false;
if needs_upgrade_or_check {
info!("Attempting to upgrade session...");
upgraded = maybe_upgrade_or_renew_session(&mut creds).await?;
info!("Attempting to check session...");
list_media(&creds).await?;
sora_creds_manager.record_credential_success()?;
}
if upgraded {
info!("Saving upgraded session to manager and disk...");
sora_creds_manager.set_credentials(&creds)?;
sora_creds_manager.persist_all_to_disk()?;
}
let maybe_account_email = creds
.jwt_bearer_token
.map(|token| token.jwt_claims().email.clone());
Ok(CheckSoraSessionCommand {
state: SoraSessionState::Valid,
maybe_account_email,
})
}
@@ -1,39 +0,0 @@
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::sora::windows::sora_login_window::open_sora_login_window::open_sora_login_window;
use errors::AnyhowResult;
use log::{error, info};
use once_cell::sync::Lazy;
use tauri::{AppHandle, State};
#[tauri::command]
pub async fn open_sora_login_command(
app: AppHandle,
app_data_root: State<'_, AppDataRoot>,
sora_creds_manager: State<'_, SoraCredentialManager>,
) -> Result<String, String> {
info!("open_login_command called");
do_open_login(&app, &app_data_root, &sora_creds_manager)
.await
.map_err(|err| {
error!("Error opening login: {:?}", err);
format!("Error opening login: {:?}", err)
})?;
Ok("result".to_string())
}
async fn do_open_login(
app: &AppHandle,
app_data_root: &AppDataRoot,
sora_creds_manager: &SoraCredentialManager,
) -> AnyhowResult<()> {
info!("Building login window...");
open_sora_login_window(app, app_data_root, sora_creds_manager).await?;
info!("Done.");
Ok(())
}
@@ -1,289 +0,0 @@
use crate::core::commands::command_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus, CommandResult};
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::simple_http_download::simple_http_download;
use crate::services::sora::events::sora_image_enqueue_failure_event::SoraImageEnqueueFailureEvent;
use crate::services::sora::events::sora_image_enqueue_success_event::SoraImageEnqueueSuccessEvent;
use crate::services::sora::state::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::services::sora::state::sora_credential_holder::SoraCredentialHolder;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use errors::{AnyhowError, AnyhowResult};
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{DynamicImage, ImageReader};
use log::{debug, error, info, warn};
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::creds::credential_migration::CredentialMigrationRef;
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use openai_sora_client::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file;
use openai_sora_client::sora_error::SoraError;
use serde_derive::{Deserialize, Serialize};
use std::fs::read_to_string;
use std::io::Cursor;
use std::ops::Add;
use std::time::Duration;
use storyteller_client::error::api_error::ApiError;
use storyteller_client::error::api_error::ApiError::InternalServerError;
use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
const SORA_IMAGE_UPLOAD_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds
const SORA_IMAGE_REMIX_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds
const DEFAULT_ASPECT_RATIO : SoraAspectRatio = SoraAspectRatio::Square;
#[derive(Deserialize, Debug)]
pub struct SoraImageRemixCommand {
/// Image media file; the engine or canvas snapshot (screenshot).
pub snapshot_media_token: MediaFileToken,
/// The user's image generation prompt.
pub prompt: String,
/// Turn off the system prompt.
pub disable_system_prompt: Option<bool>,
/// Additional images to include (optional). Up to nine images.
pub maybe_additional_images: Option<Vec<MediaFileToken>>,
pub maybe_number_of_samples: Option<u32>,
/// Aspect ratio.
pub aspect_ratio: Option<SoraAspectRatio>,
}
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum SoraAspectRatio {
Square,
Wide,
Tall,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum SoraImageRemixErrorType {
/// Generic server error
ServerError,
/// The user is sending too many requests
TooManyConcurrentTasks,
/// User is not logged into Sora!
SoraLoginRequired,
/// The user needs to create a Sora account
SoraUsernameNotYetCreated,
/// The Sora service is having problems. Try again soon.
SoraIsHavingProblems,
}
#[tauri::command]
pub async fn sora_image_remix_command(
app: AppHandle,
request: SoraImageRemixCommand,
app_data_root: State<'_, AppDataRoot>,
sora_creds_manager: State<'_, SoraCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
) -> CommandResult<(), SoraImageRemixErrorType, ()> {
info!("image_generation_command called; scene media token: {:?}, additional images: {:?}, full request: {:?}",
&request.snapshot_media_token, &request.maybe_additional_images, &request);
// TODO(bt,2025-04-24): Better error messages to caller
let has_credentials = sora_creds_manager
.has_apparently_complete_credentials()
.unwrap_or(true); // NB: Failures would be lock issues
if !has_credentials {
warn!("No apparently completed credentials found");
return Err(CommandErrorResponseWrapper {
status: CommandErrorStatus::Unauthorized,
error_message: Some("You need to log into Sora to continue. See the settings menu.".to_string()),
error_type: Some(SoraImageRemixErrorType::SoraLoginRequired),
error_details: None,
});
}
let result = generate_image(request, &app_data_root, &sora_creds_manager, &sora_task_queue).await;
match result {
Err(err) => {
error!("error: {:?}", err);
let event = SoraImageEnqueueFailureEvent {};
let result = event.send(&app);
if let Err(err) = result {
error!("Failed to emit event: {:?}", err);
}
let mut status = CommandErrorStatus::ServerError;
let mut error_type = SoraImageRemixErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
match (err) {
InnerError::SoraError(SoraError::TooManyConcurrentTasks) => {
error_type = SoraImageRemixErrorType::TooManyConcurrentTasks;
status = CommandErrorStatus::ServerError;
error_message = "You already have work in progress. Please wait for it to finish.";
},
InnerError::SoraError(SoraError::SoraUsernameNotYetCreated) => {
error_type = SoraImageRemixErrorType::SoraUsernameNotYetCreated;
status = CommandErrorStatus::BadRequest;
error_message = "You need to create a username on Sora.com to continue.";
},
InnerError::SoraError(SoraError::BadGateway(_) | SoraError::CloudFlareTimeout(_)) => {
error_type = SoraImageRemixErrorType::SoraIsHavingProblems;
status = CommandErrorStatus::ServerError;
error_message = "Sora is having problems. Please wait a moment and then retry.";
}
_ => {},
}
Err(CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
})
}
Ok(_) => {
let event = SoraImageEnqueueSuccessEvent {};
let result = event.send(&app);
if let Err(err) = result {
error!("Failed to emit event: {:?}", err);
}
Ok(().into())
}
}
}
#[derive(Debug)]
enum InnerError {
SoraError(SoraError),
AnyhowError(AnyhowError),
StorytellerApiError(ApiError),
}
impl From<AnyhowError> for InnerError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<SoraError> for InnerError {
fn from(value: SoraError) -> Self {
Self::SoraError(value)
}
}
impl From<ApiError> for InnerError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
}
}
pub async fn generate_image(
request: SoraImageRemixCommand,
app_data_root: &AppDataRoot,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<(), InnerError> {
let response = get_media_file(&ApiHost::Storyteller, &request.snapshot_media_token).await?;
let media_file_url = &response.media_file.media_links.cdn_url;
let extension_with_dot = get_url_file_extension(media_file_url)
.map(|ext| format!(".{}", ext))
.unwrap_or_else(|| ".png".to_string());
let filename = format!("{}{}", response.media_file.token.as_str(), extension_with_dot);
let filename = app_data_root.downloads_dir().path().join(&filename);
simple_http_download(&media_file_url, &filename).await?;
let files_to_upload = vec![filename];
let mut creds = sora_creds_manager.get_credentials_required()?;
let credential_updated = maybe_upgrade_or_renew_session(&mut creds)
.await
.map_err(|err| {
error!("Failed to upgrade or renew session: {:?}", err);
err
})?;
if credential_updated {
info!("Storing updated credentials");
sora_creds_manager.set_credentials(&creds)?;
}
let mut sora_media_tokens = Vec::with_capacity(files_to_upload.len());
for (i, file_path) in files_to_upload.iter().enumerate() {
info!("Uploading image {} of {}...", (i+1), files_to_upload.len());
let (response, maybe_new_credentials) =
image_upload_from_file_with_session_auto_renew(ImageUploadFromFileAutoRenewRequest {
file_path,
credentials: &creds,
request_timeout: Some(SORA_IMAGE_UPLOAD_TIMEOUT),
}).await?;
if let Some(new_creds) = maybe_new_credentials {
info!("Storing updated credentials.");
sora_creds_manager.set_credentials(&new_creds)?;
creds = new_creds;
}
sora_media_tokens.push(response.id);
}
let aspect_ratio = request.aspect_ratio.unwrap_or(DEFAULT_ASPECT_RATIO);
let aspect_ratio = match aspect_ratio {
SoraAspectRatio::Square => ImageSize::Square,
SoraAspectRatio::Wide => ImageSize::Wide,
SoraAspectRatio::Tall => ImageSize::Tall,
};
info!("Calling image generation...");
// TODO(bt,2025-04-21): Download media tokens.
// Note: This is incredibly inefficient. We should keep a local cache.
// Also, if they've already been uploaded to OpenAI, we shouldn't continue to re-upload.
let (response, maybe_new_creds) =
image_remix_with_session_auto_renew(ImageRemixAutoRenewRequest {
prompt: request.prompt.to_string(),
num_images: NumImages::One,
image_size: aspect_ratio,
sora_media_tokens: sora_media_tokens.clone(),
credentials: &creds,
request_timeout: Some(SORA_IMAGE_REMIX_TIMEOUT),
}).await?;
if let Some(new_creds) = maybe_new_creds {
info!("Storing updated credentials.");
sora_creds_manager.set_credentials(&new_creds)?;
}
info!("New Sora Task ID: {:?} ", response.task_id);
sora_task_queue.insert(&response.task_id)?;
Ok(())
}
@@ -1,39 +0,0 @@
use crate::core::commands::app_preferences::get_app_preferences_command::AppPreferencesPayload;
use crate::core::commands::command_response_wrapper::{CommandResult, CommandSuccessResponseWrapper, SimpleCommandResult};
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::sora::windows::sora_login_window::open_sora_login_window::open_sora_login_window;
use errors::{AnyhowError, AnyhowResult};
use log::{error, info};
use once_cell::sync::Lazy;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::requests::list_media::list_media::list_media;
use serde_derive::Serialize;
use std::fmt::format;
use tauri::{AppHandle, State};
#[tauri::command]
pub async fn sora_logout_command(
sora_creds_manager: State<'_, SoraCredentialManager>,
) -> SimpleCommandResult {
info!("sora_logout_command called");
if let Err(err) = sora_creds_manager.purge_credentials_from_disk() {
error!("Error purging credentials from disk: {:?}", err);
return Err("error purging credentials from disk".into());
}
if let Err(err) = sora_creds_manager.clear_credentials() {
error!("Error clearing credentials from memory: {:?}", err);
return Err("error clearing credentials from memory".into());
}
if let Err(err) = sora_creds_manager.reset_credential_stats() {
error!("Error resetting credential stats: {:?}", err);
return Err("error resetting credential stats".into());
}
Ok(().into())
}
@@ -1,5 +0,0 @@
pub mod sora_image_enqueue_failure_event;
pub mod sora_image_enqueue_success_event;
pub mod sora_image_generation_complete_event;
pub mod sora_image_generation_failed_event;
pub mod sora_login_success_event;
@@ -1,11 +0,0 @@
use crate::core::events::sendable_event_trait::SendableEvent;
use serde_derive::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageEnqueueFailureEvent {
// TODO: Reason.
}
impl SendableEvent for SoraImageEnqueueFailureEvent {
const FRONTEND_EVENT_NAME: &'static str = "sora-image-enqueue-failure";
}
@@ -1,11 +0,0 @@
use crate::core::events::sendable_event_trait::SendableEvent;
use serde_derive::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageEnqueueSuccessEvent {
// TODO: Token, etc.
}
impl SendableEvent for SoraImageEnqueueSuccessEvent {
const FRONTEND_EVENT_NAME: &'static str = "sora-image-enqueue-success";
}
@@ -1,12 +0,0 @@
use serde_derive::Serialize;
use tokens::tokens::media_files::MediaFileToken;
use crate::core::events::sendable_event_trait::SendableEvent;
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageGenerationCompleteEvent {
pub media_file_token: MediaFileToken,
}
impl SendableEvent for SoraImageGenerationCompleteEvent {
const FRONTEND_EVENT_NAME: &'static str = "sora-image-generation-complete";
}
@@ -1,12 +0,0 @@
use serde_derive::Serialize;
use crate::core::events::sendable_event_trait::SendableEvent;
#[derive(Debug, Clone, Serialize)]
pub struct SoraImageGenerationFailedEvent {
pub prompt: String,
}
impl SendableEvent for SoraImageGenerationFailedEvent {
const FRONTEND_EVENT_NAME: &'static str = "sora-image-generation-failed";
}
@@ -1,11 +0,0 @@
use crate::core::events::sendable_event_trait::SendableEvent;
use serde_derive::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct SoraLoginSuccessEvent {
// TODO: Reason.
}
impl SendableEvent for SoraLoginSuccessEvent {
const FRONTEND_EVENT_NAME: &'static str = "sora-login-success";
}
@@ -1,5 +0,0 @@
pub mod commands;
pub mod events;
pub mod state;
pub mod threads;
pub mod windows;
@@ -1,47 +0,0 @@
/// This is meant to track 401s and other credential failures, not 500s.
#[derive(Clone)]
pub struct SoraCredentialStats {
pub last_credential_failure: Option<chrono::DateTime<chrono::Utc>>,
pub last_credential_success: Option<chrono::DateTime<chrono::Utc>>,
pub credential_success_count: u32,
pub credential_failure_count: u32,
/// This resets on each failure
pub last_consecutive_credential_success: Option<chrono::DateTime<chrono::Utc>>,
/// This resets on each failure
pub consecutive_credential_success_count: u32,
/// This resets on each success
pub consecutive_credential_failure_count: u32,
}
impl SoraCredentialStats {
pub fn new() -> Self {
Self {
last_credential_failure: None,
last_credential_success: None,
last_consecutive_credential_success: None,
credential_success_count: 0,
credential_failure_count: 0,
consecutive_credential_success_count: 0,
consecutive_credential_failure_count: 0,
}
}
pub fn record_credential_success(&mut self) {
let now = chrono::Utc::now();
self.last_credential_success = Some(now);
self.last_consecutive_credential_success = Some(now);
self.credential_success_count += 1;
self.consecutive_credential_success_count += 1;
self.consecutive_credential_failure_count = 0;
}
pub fn record_credential_failure(&mut self) {
let now = chrono::Utc::now();
self.last_credential_failure = Some(now);
self.last_consecutive_credential_success = None;
self.credential_failure_count += 1;
self.consecutive_credential_failure_count += 1;
self.consecutive_credential_success_count = 0;
}
}
@@ -1 +0,0 @@
pub mod sora_task_polling_thread;
@@ -1 +0,0 @@
pub mod sora_login_window;
@@ -1,20 +0,0 @@
use anyhow::anyhow;
use log::info;
use tauri::Webview;
use errors::AnyhowResult;
// This is just so we have a way to clear browsing data while debugging / building.
pub fn clear_browsing_data_on_test_domain(webview: &Webview) -> AnyhowResult<()> {
let url = webview.url()?;
let hostname= url.host()
.ok_or(anyhow!("no host in url"))?
.to_string();
match hostname.as_str() {
"storyteller.ai" => {
info!("Clearing all browsing data...");
webview.clear_all_browsing_data()?;
}
_ => {}
}
Ok(())
}
@@ -1,28 +0,0 @@
use errors::AnyhowResult;
use once_cell::sync::Lazy;
use reqwest::Url;
use tauri::webview::Cookie;
use tauri::WebviewWindow;
const CHAT_GPT_ROOT_COOKIE_URL_STR: &str = "https://chatgpt.com";
static CHAT_GPT_ROOT_COOKIE_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(CHAT_GPT_ROOT_COOKIE_URL_STR).expect("URL should parse")
});
pub fn extract_sora_webview_cookies(webview: &WebviewWindow) -> AnyhowResult<String> {
// NB(bt): Cookies were originally on sora.com, but now they're located on
// the sora.chatgpt.com / *.chatgpt.com domains.
let cookies = get_all_chatgpt_cookies(webview)?;
let cookie_string = cookies
.iter()
.map(|cookie| format!("{}={}", cookie.name(), cookie.value()))
.collect::<Vec<String>>()
.join("; ");
Ok(cookie_string)
}
fn get_all_chatgpt_cookies(webview: &WebviewWindow) -> AnyhowResult<Vec<Cookie>> {
let cookies = webview.cookies_for_url(CHAT_GPT_ROOT_COOKIE_URL.clone())?;
Ok(cookies)
}
@@ -1,28 +0,0 @@
use crate::services::sora::windows::sora_login_window::open_sora_login_window::SORA_LOGIN_URL;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::info;
use tauri::Webview;
// NB: Probably best not to use this.
pub fn keep_on_login_domains(webview: &Webview) -> AnyhowResult<()> {
let url = webview.url()?;
let hostname= url.host()
.ok_or(anyhow!("no host in url"))?
.to_string();
match hostname.as_str() {
"auth.openai.com" => {},
"openai.com" => {},
"sora.com" => {},
// Third party SSO
"accounts.google.com" => {},
"accounts.youtube.com" => {},
"login.live.com" => {},
"appleid.apple.com" => {},
_ => {
info!("Non login hostname: {}", hostname);
webview.navigate(SORA_LOGIN_URL.clone())?;
}
}
Ok(())
}
@@ -1,5 +0,0 @@
pub mod clear_browsing_on_test_domain;
pub mod extract_sora_webview_cookies;
pub mod keep_on_login_domains;
pub mod open_sora_login_window;
pub mod sora_login_thread;
@@ -1,84 +0,0 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::core::utils::clear_all_webview_cookies::clear_all_webview_cookies;
use crate::services::sora::windows::sora_login_window::sora_login_thread::sora_login_thread;
use anyhow::anyhow;
use errors::AnyhowResult;
use once_cell::sync::Lazy;
use reqwest::Url;
use std::time::Duration;
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
/// Name of the window
pub const LOGIN_WINDOW_NAME: &str = "login_window";
pub const START_URL_STR: &str = "https://google.com";
pub static START_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(START_URL_STR).expect("URL should parse")
});
pub const SORA_HOMEPAGE_URL_STR: &str = "https://sora.com/";
pub static SORA_HOMEPAGE_URL : Lazy<Url> = Lazy::new(|| {
Url::parse(SORA_HOMEPAGE_URL_STR).expect("URL should parse")
});
pub const SORA_LOGIN_URL_STR: &str = "https://chatgpt.com/auth/login?next=%2Fsora%2F";
pub static SORA_LOGIN_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse")
});
//pub const SORA_ROOT_URL_STR: &str = "https://sora.com/";
//
//pub static SORA_ROOT_URL: Lazy<Url> = Lazy::new(|| {
// Url::parse(SORA_ROOT_URL_STR).expect("URL should parse")
//});
pub async fn open_sora_login_window(
app: &AppHandle,
app_data_root: &AppDataRoot,
sora_creds_manager: &SoraCredentialManager,
) -> AnyhowResult<()> {
if app.get_window(LOGIN_WINDOW_NAME).is_some() {
return Err(anyhow!("Login window already open"));
}
let url = WebviewUrl::External(START_URL.clone());
let window = WebviewWindowBuilder::new(app, LOGIN_WINDOW_NAME, url)
//.user_agent(openai_sora_client::credentials::USER_AGENT)
.always_on_top(true)
.title("Login to OpenAI's Sora")
.center()
.resizable(true)
.visible(true)
.closable(true)
.min_inner_size(200.0, 800.0)
.focused(true)
.devtools(true)
.build()?;
let webview = window.get_webview(LOGIN_WINDOW_NAME)
.ok_or_else(|| anyhow!("no webview found"))?;
clear_all_webview_cookies(&webview)?;
webview.navigate(SORA_HOMEPAGE_URL.clone())?;
// NB: We're starting to get Cloudflare protection screens. Let's try to avoid.
tokio::time::sleep(Duration::from_millis(100)).await;
webview.navigate(SORA_LOGIN_URL.clone())?;
let app_handle = app.clone();
let app_data_root = app_data_root.clone();
let sora_creds_manager = sora_creds_manager.clone();
let _ = tauri::async_runtime::spawn(async move {
sora_login_thread(app_handle, app_data_root, sora_creds_manager).await;
});
Ok(())
}
@@ -1,157 +0,0 @@
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::services::sora::events::sora_login_success_event::SoraLoginSuccessEvent;
use crate::services::sora::state::initialize_sora_jwt_bearer_token::initialize_sora_jwt_bearer_token;
use crate::services::sora::state::sora_credential_holder::SoraCredentialHolder;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::windows::sora_login_window::extract_sora_webview_cookies::extract_sora_webview_cookies;
use crate::services::sora::windows::sora_login_window::open_sora_login_window::LOGIN_WINDOW_NAME;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use anyhow::anyhow;
use chrono::{DateTime, NaiveDateTime, TimeDelta};
use errors::AnyhowResult;
use log::{error, info, warn};
use once_cell::sync::Lazy;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::utils::has_session_cookie::{has_session_cookie, SessionCookiePresence};
use reqwest::Url;
use std::fs;
use std::fs::{read_to_string, OpenOptions};
use std::io::Write;
use std::ops::Sub;
use tauri::{AppHandle, Manager, Webview, WebviewWindow};
pub async fn sora_login_thread(
app: AppHandle,
app_data_root: AppDataRoot,
sora_creds_manager: SoraCredentialManager
) {
loop {
let login_webview_window = match app.get_webview_window(LOGIN_WINDOW_NAME) {
Some(webview) => webview,
None => {
info!("Exit sora login thread.");
return; // NB: Only exit if we don't have the webview.
}
};
let result = check_login_window(
&app,
&login_webview_window,
&app_data_root,
&sora_creds_manager,
).await;
match result {
Err(err) => {
error!("Error checking login window: {:?}", err);
}
Ok(false) => {} // Continue iteration and try again...
Ok(true) => {
info!("Successfully saved cookies from login window. Closing.");
if let Err(err) = login_webview_window.close() {
error!("Error closing login window: {:?}", err);
}
return;
}
}
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
}
}
/// Returns true if we can exit.
async fn check_login_window(
app_handle: &AppHandle,
webview_window: &WebviewWindow,
app_data_root: &AppDataRoot,
sora_credential_manager: &SoraCredentialManager,
) -> AnyhowResult<bool> {
/* Login flow looks like this:
1. Start: https://sora.chatgpt.com/
2. Login Start: https://chatgpt.com/auth/login?next=%2Fsora%2F [this is where we start]
3. Login Continue: https://auth.openai.com/log-in
4. SSO / Google Login (etc): https://accounts.google.com/v3/signin/challenge/pwd?[query]
5. Done / Landing: https://sora.chatgpt.com/explore
*/
/*
- Do not use credential manager until the end (we don't load old cookies!)
- Check if we're on the correct domain, if not exit? (or inverse, that we're not in login flow)
*/
let hostname = get_hostname(webview_window)?;
let mut maybe_at_destination = false;
match hostname.as_str() {
"sora.com" | // Old destination
"sora.chatgpt.com" // New destination
=> {
maybe_at_destination = true;
}
// chatgpt.com/auth is also an auth domain
"auth.openai.com" |
"accounts.google.com" |
"accounts.youtube.com" |
"login.live.com" |
"appleid.apple.com"
=> {
// NB: We're in auth flow.
info!("Sora login webview is in auth flow; hostname `{}`.", hostname);
return Ok(false)
}
_ => {}, // We just don't know...
}
let webview_cookies = extract_sora_webview_cookies(webview_window)?.trim().to_string();
let session_cookie_presence = has_session_cookie(&webview_cookies)
.unwrap_or_else(|err| {
error!("Failed to check for session cookie: {:?}", err);
SessionCookiePresence::MaybePresent
});
info!("Sora login webview is at hostname `{}`; cookie status: {:?}.", hostname, session_cookie_presence);
match session_cookie_presence {
SessionCookiePresence::Absent => {
info!("Session cookies are absent.");
return Ok(false);
}
_ => {},
}
let mut new_credentials =
SoraCredentialSet::initialize_with_just_cookies_str(&webview_cookies);
let _upgraded = maybe_upgrade_or_renew_session(&mut new_credentials).await?;
// TODO(bt): Race conditions ahead.
sora_credential_manager.clear_credentials()?;
sora_credential_manager.try_purge_credentials_from_disk();
sora_credential_manager.set_credentials(&new_credentials)?;
sora_credential_manager.persist_all_to_disk()?;
// NB: Event sent to the frontend for the login flow. We shouldn't rely on just this
// alone as it could be brittle if the events aren't caught.
let event = SoraLoginSuccessEvent {};
if let Err(err) = event.send(app_handle) {
error!("Error sending Sora login success event: {:?}", err);
}
Ok(true)
}
fn get_hostname(webview: &WebviewWindow) -> AnyhowResult<String> {
let url = webview.url()?;
let url_hostname= url.host()
.ok_or(anyhow!("no host in url"))?
.to_string();
Ok(url_hostname)
}
@@ -1 +0,0 @@
pub mod state;
@@ -1,6 +1,6 @@
use crate::core::state::app_preferences::app_preferences_serializable::AppPreferencesSerializable;
use crate::core::state::app_preferences::preferred_download_directory::{PreferredDownloadDirectory, SystemDownloadDirectory};
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::state::app_preferences::app_preferences_serializable::AppPreferencesSerializable;
use crate::state::app_preferences::preferred_download_directory::{PreferredDownloadDirectory, SystemDownloadDirectory};
use crate::state::data_dir::app_data_root::AppDataRoot;
#[derive(Clone)]
pub struct AppPreferences {
@@ -9,14 +9,6 @@ pub struct AppPreferences {
/// Play sounds on events.
pub play_sounds: bool,
/// Key pointing to file; defined in the frontend code.
/// Defined for enqueue since image enqueue can be async
pub enqueue_success_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
/// Defined for enqueue since image enqueue can be async
pub enqueue_failure_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
pub generation_success_sound: Option<String>,
@@ -25,7 +17,6 @@ pub struct AppPreferences {
pub generation_failure_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
#[deprecated]
pub generation_enqueue_sound: Option<String>,
}
@@ -35,9 +26,7 @@ impl Default for AppPreferences {
preferred_download_directory: PreferredDownloadDirectory::System(SystemDownloadDirectory::Downloads),
play_sounds: true,
// NB: These are defined in the frontend.
enqueue_success_sound: Some("done".to_string()),
enqueue_failure_sound: Some("spike_throw".to_string()),
generation_success_sound: Some("special_flower".to_string()),
generation_success_sound: Some("flower".to_string()),
generation_failure_sound: Some("crumble".to_string()),
generation_enqueue_sound: Some("done".to_string()),
}
@@ -1,5 +1,5 @@
use crate::core::state::app_preferences::app_preferences::AppPreferences;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::state::app_preferences::app_preferences::AppPreferences;
use crate::state::data_dir::app_data_root::AppDataRoot;
use memory_store::clone_cell::CloneCell;
pub type AppPreferencesManager = CloneCell<AppPreferences>;
@@ -1,6 +1,6 @@
use crate::core::state::app_preferences::app_preferences::AppPreferences;
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::state::app_preferences::app_preferences::AppPreferences;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
@@ -58,8 +58,8 @@ impl Display for SystemDownloadDirectory {
#[cfg(test)]
mod tests {
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::core::state::app_preferences::preferred_download_directory::SystemDownloadDirectory;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::state::app_preferences::preferred_download_directory::SystemDownloadDirectory;
use std::str::FromStr;
mod json {
@@ -1,4 +1,4 @@
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use chrono::{DateTime, Local};
use std::path::{Path, PathBuf};
@@ -1,4 +1,4 @@
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
#[derive(Clone)]
@@ -40,8 +40,4 @@ impl AppCredentialsDir {
pub fn get_storyteller_session_cookie_file_path(&self) -> PathBuf {
self.path.join("artcraft_session.txt")
}
pub fn get_fal_api_key_file_path(&self) -> PathBuf {
self.path.join("fal_api_key.txt")
}
}
@@ -1,12 +1,12 @@
use crate::core::state::data_dir::app_assets_dir::AppAssetsDir;
use crate::core::state::data_dir::app_credentials_dir::AppCredentialsDir;
use crate::core::state::data_dir::app_downloads_dir::AppDownloadsDir;
use crate::core::state::data_dir::app_settings_dir::AppSettingsDir;
use crate::core::state::data_dir::app_weights_dir::AppWeightsDir;
use crate::core::state::data_dir::temporary_dir::TemporaryDir;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::expanduser::expanduser;
use crate::core::state::os_platform::OsPlatform;
use crate::state::data_dir::app_assets_dir::AppAssetsDir;
use crate::state::data_dir::app_credentials_dir::AppCredentialsDir;
use crate::state::data_dir::app_downloads_dir::AppDownloadsDir;
use crate::state::data_dir::app_settings_dir::AppSettingsDir;
use crate::state::data_dir::app_weights_dir::AppWeightsDir;
use crate::state::data_dir::temporary_dir::TemporaryDir;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::expanduser::expanduser;
use crate::state::os_platform::OsPlatform;
use anyhow::anyhow;
use directories::UserDirs;
use std::path::{Path, PathBuf};
@@ -136,10 +136,6 @@ impl AppDataRoot {
pub fn get_window_size_config_file(&self) -> PathBuf {
self.path.join("window_size.json")
}
pub fn get_window_position_config_file(&self) -> PathBuf {
self.path.join("window_position.json")
}
}
// eg. /home/bob/artcraft, /Users/bob/artcraft, or C:\Users\Alice\artcraft
@@ -1,4 +1,4 @@
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
#[derive(Clone)]
@@ -1,5 +1,5 @@
use crate::core::state::app_preferences::app_preferences::AppPreferences;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::app_preferences::app_preferences::AppPreferences;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use errors::AnyhowResult;
use serde::Serialize;
use std::fs::OpenOptions;
@@ -1,4 +1,4 @@
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
#[derive(Clone)]
@@ -1,4 +1,4 @@
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
use tempdir::TempDir;
use tempfile::{Builder, NamedTempFile};
@@ -35,17 +35,4 @@ impl TemporaryDir {
.tempfile_in(&self.path)?;
Ok(tempfile)
}
pub fn new_named_temp_file_with_extension(&self, extension: &str) -> Result<NamedTempFile, std::io::Error> {
let extension = if extension.starts_with(".") {
extension.to_string()
} else {
format!(".{}", extension)
};
let tempfile = Builder::new()
.prefix("temp_")
.suffix(&extension)
.tempfile_in(&self.path)?;
Ok(tempfile)
}
}
@@ -43,7 +43,7 @@ pub fn home_dir() -> io::Result<PathBuf> {
#[cfg(test)]
mod tests {
use crate::core::state::expanduser::expanduser;
use crate::state::expanduser::expanduser;
use std::env;
use std::path::PathBuf;
@@ -1,12 +1,12 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::best_window_size_heuristic::best_window_size_heuristic;
use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME;
use crate::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
use std::fs::OpenOptions;
use std::io::Write;
use tauri::{AppHandle, Manager, PhysicalSize, Window};
const MAIN_WINDOW_NAME: &str = "main";
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct MainWindowSize {
pub width: u32,
@@ -26,7 +26,7 @@ impl MainWindowSize {
}
pub fn from_window(window: &Window) -> AnyhowResult<Self> {
let size = best_window_size_heuristic(window)?;
let size = window.inner_size()?;
Ok(Self {
width: size.width,
height: size.height,
@@ -2,6 +2,7 @@ pub mod app_preferences;
pub mod app_startup_time;
pub mod data_dir;
pub mod expanduser;
pub mod main_window_position;
pub mod main_window_size;
pub mod os_platform;
pub mod os_platform;
pub mod sora;
pub mod storyteller;
@@ -1,7 +1,5 @@
pub mod initialize_sora_jwt_bearer_token;
pub mod read_sora_credentials_from_disk;
pub mod read_sora_credentials_from_disk_with_upgrade;
pub mod sora_credential_holder;
pub mod sora_credential_manager;
pub mod sora_credential_stats;
pub mod sora_task_queue;
@@ -1,4 +1,4 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::app_data_root::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use openai_sora_client::creds::sora_cookies::SoraCookies;
@@ -1,4 +1,4 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::app_data_root::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{info, warn};
@@ -54,20 +54,4 @@ impl SoraCredentialHolder {
}
}
}
/// This does not guarantee the credentials are correct, just that they're set.
pub fn has_apparently_complete_credentials(&self) -> AnyhowResult<bool> {
match self.credentials.read() {
Err(err) => Err(anyhow!("Failed to acquire read lock: {:?}", err)),
Ok(creds) => match &*creds {
None => Ok(false),
Some(creds) => {
let has_cookies = !creds.cookies.as_str().is_empty();
let has_bearer = creds.jwt_bearer_token.is_some();
let has_sentinel = creds.sora_sentinel.is_some();
Ok(has_cookies && has_bearer && has_sentinel)
},
}
}
}
}
@@ -1,29 +1,21 @@
use crate::services::sora::state::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::services::sora::state::sora_credential_holder::SoraCredentialHolder;
use crate::services::sora::state::sora_credential_stats::SoraCredentialStats;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{error, info, warn};
use memory_store::clone_cell::CloneCell;
use openai_sora_client::creds::sora_cookies::SoraCookies;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
use openai_sora_client::creds::sora_sentinel::SoraSentinel;
use openai_sora_client::requests::sentinel_refresh::generate::token::generate_token;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use std::{fs, io};
use openai_sora_client::creds::sora_cookies::SoraCookies;
use openai_sora_client::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
#[derive(Clone)]
pub struct SoraCredentialManager {
holder: SoraCredentialHolder,
app_data_root: AppDataRoot,
/// This is meant to help debug credential issues, keep credentials cached, fresh, etc.
// TODO: Put this into the same structure as the credentials and also persist to disk.
stats: CloneCell<SoraCredentialStats>,
}
impl SoraCredentialManager {
@@ -41,41 +33,9 @@ impl SoraCredentialManager {
Self {
holder,
app_data_root: app_data_root.clone(),
stats: CloneCell::with_owned(SoraCredentialStats::new()),
}
}
pub fn record_credential_success(&self) -> AnyhowResult<()> {
let mut stats = self.stats.get_clone()?;
stats.record_credential_success();
self.stats.set_owned(stats)?;
Ok(())
}
pub fn record_credential_failure(&self) -> AnyhowResult<()> {
let mut stats = self.stats.get_clone()?;
stats.record_credential_failure();
self.stats.set_owned(stats)?;
Ok(())
}
pub fn reset_credential_stats(&self) -> AnyhowResult<()> {
self.stats.set_owned(SoraCredentialStats::new())
}
/// This does not guarantee the credentials are correct
pub fn has_apparently_complete_credentials(&self) -> AnyhowResult<bool> {
self.holder.has_apparently_complete_credentials()
}
pub fn get_credential_stats(&self) -> AnyhowResult<SoraCredentialStats> {
self.stats.get_clone()
}
pub fn set_credential_stats(&self, stats: SoraCredentialStats) -> AnyhowResult<()> {
self.stats.set_owned(stats)
}
pub fn set_credentials(&self, creds: &SoraCredentialSet) -> AnyhowResult<()> {
self.holder.set_credentials(creds)
}
@@ -85,31 +45,6 @@ impl SoraCredentialManager {
Ok(())
}
/// This is meant to be infallible.
pub fn purge_credentials_from_disk(&self) -> Result<(), io::Error> {
let creds_dir = self.app_data_root.credentials_dir();
remove_file_if_exists(creds_dir.get_sora_cookie_file_path())?;
remove_file_if_exists(creds_dir.get_sora_bearer_token_file_path())?;
remove_file_if_exists(creds_dir.get_sora_sentinel_file_path())?;
Ok(())
}
/// This is meant to be infallible.
pub fn try_purge_credentials_from_disk(&self) {
let creds_dir = self.app_data_root.credentials_dir();
if let Err(err) = fs::remove_file(creds_dir.get_sora_cookie_file_path()) {
error!("Failed to remove cookie file: {:?}", err);
}
if let Err(err) = fs::remove_file(creds_dir.get_sora_bearer_token_file_path()) {
error!("Failed to remove bearer token file: {:?}", err);
}
if let Err(err) = fs::remove_file(creds_dir.get_sora_sentinel_file_path()) {
error!("Failed to remove sentinel file: {:?}", err);
}
}
pub fn get_credentials(&self) -> AnyhowResult<Option<SoraCredentialSet>> {
self.holder.get_credentials()
}
@@ -220,11 +155,3 @@ fn persist_sentinel_to_disk(sentinel: &SoraSentinel, app_data_root: &AppDataRoot
Ok(())
}
fn remove_file_if_exists<P: AsRef<Path>>(path: P) -> Result<(), io::Error> {
let p = path.as_ref();
if p.exists() && p.is_file() {
fs::remove_file(p)?;
}
Ok(())
}
@@ -1,8 +1,8 @@
use std::collections::HashSet;
use chrono::{DateTime, Utc};
use errors::AnyhowResult;
use indexmap::IndexMap;
use openai_sora_client::requests::image_gen::image_gen_status::TaskId;
use std::collections::HashSet;
use std::sync::{Arc, RwLock};
#[derive(Clone)]
@@ -152,9 +152,9 @@ impl SoraTaskQueue {
#[cfg(test)]
mod tests {
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use errors::AnyhowResult;
use openai_sora_client::requests::image_gen::image_gen_status::TaskId;
use crate::state::sora::sora_task_queue::SoraTaskQueue;
#[test]
fn remove_list() -> AnyhowResult<()> {
@@ -1,4 +1,4 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use std::fs::read_to_string;
use storyteller_client::credentials::storyteller_avt_cookie::StorytellerAvtCookie;
@@ -1,8 +1,8 @@
use crate::services::sora::state::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::services::sora::state::sora_credential_holder::SoraCredentialHolder;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::storyteller::state::read_storyteller_credentials_from_disk::read_storyteller_credentials_from_disk;
use crate::services::storyteller::state::storyteller_credential_holder::StorytellerCredentialHolder;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use crate::state::storyteller::read_storyteller_credentials_from_disk::read_storyteller_credentials_from_disk;
use crate::state::storyteller::storyteller_credential_holder::StorytellerCredentialHolder;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{error, info, warn};
@@ -1,7 +1,7 @@
use anyhow::anyhow;
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
use errors::AnyhowResult;
use log::{debug, info};
use log::info;
/// Our Discord App ID. Not sure if this is a secret.
const DISCORD_APP_ID : &str = "1366596912593113138";
@@ -15,13 +15,13 @@ pub async fn discord_presence_thread() -> ! {
}
async fn discord_main_loop(mut client: DiscordIpcClient) -> AnyhowResult<()> {
info!("(Re)-connecting Discord IPC client...");
info!("Connecting Discord IPC client");
client.connect()
.map_err(|err| anyhow!("Error connecting to Discord IPC: {:?}", err))?;
loop {
debug!("Notifying discord presence API...");
info!("Notifying discord...");
let assets = discord_rich_presence::activity::Assets::new()
.large_image("https://storyteller.ai/android-chrome-512x512.png")
@@ -37,6 +37,6 @@ async fn discord_main_loop(mut client: DiscordIpcClient) -> AnyhowResult<()> {
.assets(assets))
.map_err(|err| anyhow!("Error setting Discord activity: {:?}", err))?;
tokio::time::sleep(std::time::Duration::from_millis(60_000)).await;
tokio::time::sleep(std::time::Duration::from_millis(30_000)).await;
}
}
@@ -1,15 +1,13 @@
use crate::core::state::app_startup_time::AppStartupTime;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_position::MainWindowPosition;
use crate::core::state::main_window_size::MainWindowSize;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use crate::core::threads::main_window_thread::persist_storyteller_cookies_task::persist_storyteller_cookies_task;
use crate::core::threads::main_window_thread::persist_window_position_task::persist_window_position_task;
use crate::core::threads::main_window_thread::persist_window_resize_task::persist_window_resize_task;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_size::MainWindowSize;
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
use crate::threads::main_window_thread::persist_storyteller_cookies_task::persist_storyteller_cookies_task;
use crate::threads::main_window_thread::persist_window_resize_task::persist_window_resize_task;
use errors::AnyhowResult;
use log::{error, info};
use memory_store::clone_slot::CloneSlot;
use tauri::{AppHandle, Manager, Webview, Window};
use crate::state::app_startup_time::AppStartupTime;
const MAIN_WINDOW_NAME : &str = "main";
@@ -20,7 +18,6 @@ pub async fn main_window_thread(
) -> ! {
// TODO: Move these into some kind of dependency injection framework
let window_size_slot: CloneSlot<MainWindowSize> = CloneSlot::empty();
let window_pos_slot: CloneSlot<MainWindowPosition> = CloneSlot::empty();
let app_startup_time = AppStartupTime::new();
loop {
@@ -32,7 +29,6 @@ pub async fn main_window_thread(
&app_data_root,
&storyteller_creds_manager,
&window_size_slot,
&window_pos_slot,
&app_startup_time,
).await;
if let Err(err) = result {
@@ -50,12 +46,10 @@ pub async fn handle_main_window(
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
window_size_slot: &CloneSlot<MainWindowSize>,
window_pos_slot: &CloneSlot<MainWindowPosition>,
app_startup_time: &AppStartupTime,
) -> AnyhowResult<()> {
loop {
log_errors(persist_window_resize_task(window, app_data_root, window_size_slot).await);
log_errors(persist_window_position_task(window, app_data_root, window_pos_slot).await);
log_errors(persist_storyteller_cookies_task(app, storyteller_creds_manager, app_startup_time).await);
tokio::time::sleep(std::time::Duration::from_millis(1_000)).await;
}

Some files were not shown because too many files have changed in this diff Show More