diff --git a/Cargo.lock b/Cargo.lock index 2381c34fef..e5f9ec03f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -850,6 +850,7 @@ dependencies = [ "tokens", "tokio", "tokio-util", + "url", "workspace-build-acceleration", ] @@ -4248,11 +4249,14 @@ name = "fal_client" version = "0.0.1" dependencies = [ "anyhow", + "chrono", "errors", "fal", "file_to_base64_url", "filesys", "futures 0.3.31", + "reqwest 0.12.15", + "serde", "testing", "tokio", "url", @@ -8802,7 +8806,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_macros 0.11.2", - "phf_shared 0.11.2", + "phf_shared 0.11.3", ] [[package]] @@ -8851,7 +8855,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ - "phf_shared 0.11.2", + "phf_shared 0.11.3", "rand 0.8.5", ] @@ -8876,7 +8880,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ "phf_generator 0.11.2", - "phf_shared 0.11.2", + "phf_shared 0.11.3", "proc-macro2", "quote", "syn 2.0.87", @@ -8888,7 +8892,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" dependencies = [ - "siphasher", + "siphasher 0.3.11", ] [[package]] @@ -8897,16 +8901,16 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" dependencies = [ - "siphasher", + "siphasher 0.3.11", ] [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher", + "siphasher 1.0.1", ] [[package]] @@ -11278,6 +11282,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -11836,6 +11846,7 @@ dependencies = [ "errors", "idempotency", "log", + "mimetypes", "reqwest 0.12.15", "serde", "serde_derive", @@ -13895,12 +13906,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" @@ -15317,7 +15325,7 @@ dependencies = [ "openssl-sys", "parking_lot 0.12.2", "phf 0.11.2", - "phf_shared 0.11.2", + "phf_shared 0.11.3", "proc-macro2", "prost", "pyo3", diff --git a/crates/desktop/artcraft/Cargo.toml b/crates/desktop/artcraft/Cargo.toml index 25bb000aa1..15a3f9ff04 100644 --- a/crates/desktop/artcraft/Cargo.toml +++ b/crates/desktop/artcraft/Cargo.toml @@ -62,6 +62,7 @@ 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" } diff --git a/crates/desktop/artcraft/src/commands/app_preferences/get_app_preferences_command.rs b/crates/desktop/artcraft/src/core/commands/app_preferences/get_app_preferences_command.rs similarity index 92% rename from crates/desktop/artcraft/src/commands/app_preferences/get_app_preferences_command.rs rename to crates/desktop/artcraft/src/core/commands/app_preferences/get_app_preferences_command.rs index 7c5b58e611..c6ce55250e 100644 --- a/crates/desktop/artcraft/src/commands/app_preferences/get_app_preferences_command.rs +++ b/crates/desktop/artcraft/src/core/commands/app_preferences/get_app_preferences_command.rs @@ -1,5 +1,5 @@ -use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager; -use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory; +use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager; +use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory; use errors::AnyhowResult; use log::{error, info}; use serde_derive::Serialize; diff --git a/crates/desktop/artcraft/src/commands/app_preferences/mod.rs b/crates/desktop/artcraft/src/core/commands/app_preferences/mod.rs similarity index 100% rename from crates/desktop/artcraft/src/commands/app_preferences/mod.rs rename to crates/desktop/artcraft/src/core/commands/app_preferences/mod.rs diff --git a/crates/desktop/artcraft/src/commands/app_preferences/update_app_preference_command.rs b/crates/desktop/artcraft/src/core/commands/app_preferences/update_app_preference_command.rs similarity index 92% rename from crates/desktop/artcraft/src/commands/app_preferences/update_app_preference_command.rs rename to crates/desktop/artcraft/src/core/commands/app_preferences/update_app_preference_command.rs index 1b16b82972..6f420d4053 100644 --- a/crates/desktop/artcraft/src/commands/app_preferences/update_app_preference_command.rs +++ b/crates/desktop/artcraft/src/core/commands/app_preferences/update_app_preference_command.rs @@ -1,6 +1,6 @@ -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 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 log::{error, info}; diff --git a/crates/desktop/artcraft/src/commands/command_response_wrapper.rs b/crates/desktop/artcraft/src/core/commands/command_response_wrapper.rs similarity index 100% rename from crates/desktop/artcraft/src/commands/command_response_wrapper.rs rename to crates/desktop/artcraft/src/core/commands/command_response_wrapper.rs diff --git a/crates/desktop/artcraft/src/commands/flip_image.rs b/crates/desktop/artcraft/src/core/commands/flip_image.rs similarity index 100% rename from crates/desktop/artcraft/src/commands/flip_image.rs rename to crates/desktop/artcraft/src/core/commands/flip_image.rs diff --git a/crates/desktop/artcraft/src/commands/mod.rs b/crates/desktop/artcraft/src/core/commands/mod.rs similarity index 80% rename from crates/desktop/artcraft/src/commands/mod.rs rename to crates/desktop/artcraft/src/core/commands/mod.rs index 60f1daaac4..ae09eb37bd 100644 --- a/crates/desktop/artcraft/src/commands/mod.rs +++ b/crates/desktop/artcraft/src/core/commands/mod.rs @@ -1,6 +1,4 @@ pub mod app_preferences; pub mod command_response_wrapper; -pub mod fal; pub mod flip_image; pub mod platform_info_command; -pub mod sora; diff --git a/crates/desktop/artcraft/src/commands/platform_info_command.rs b/crates/desktop/artcraft/src/core/commands/platform_info_command.rs similarity index 97% rename from crates/desktop/artcraft/src/commands/platform_info_command.rs rename to crates/desktop/artcraft/src/core/commands/platform_info_command.rs index f4e9901281..b658be56ac 100644 --- a/crates/desktop/artcraft/src/commands/platform_info_command.rs +++ b/crates/desktop/artcraft/src/core/commands/platform_info_command.rs @@ -1,4 +1,4 @@ -use crate::state::os_platform::OsPlatform; +use crate::core::state::os_platform::OsPlatform; use base64::prelude::BASE64_STANDARD; use base64::Engine; use image::codecs::png::{CompressionType, FilterType, PngEncoder}; diff --git a/crates/desktop/artcraft/src/events/mod.rs b/crates/desktop/artcraft/src/core/events/mod.rs similarity index 57% rename from crates/desktop/artcraft/src/events/mod.rs rename to crates/desktop/artcraft/src/core/events/mod.rs index ee9e0cd166..23955acbf0 100644 --- a/crates/desktop/artcraft/src/events/mod.rs +++ b/crates/desktop/artcraft/src/core/events/mod.rs @@ -1,4 +1,3 @@ pub mod notification_event; pub mod sendable_event_error; -pub mod sendable_event_trait; -pub mod sora; \ No newline at end of file +pub mod sendable_event_trait; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/events/notification_event.rs b/crates/desktop/artcraft/src/core/events/notification_event.rs similarity index 100% rename from crates/desktop/artcraft/src/events/notification_event.rs rename to crates/desktop/artcraft/src/core/events/notification_event.rs diff --git a/crates/desktop/artcraft/src/events/sendable_event_error.rs b/crates/desktop/artcraft/src/core/events/sendable_event_error.rs similarity index 100% rename from crates/desktop/artcraft/src/events/sendable_event_error.rs rename to crates/desktop/artcraft/src/core/events/sendable_event_error.rs diff --git a/crates/desktop/artcraft/src/events/sendable_event_trait.rs b/crates/desktop/artcraft/src/core/events/sendable_event_trait.rs similarity index 91% rename from crates/desktop/artcraft/src/events/sendable_event_trait.rs rename to crates/desktop/artcraft/src/core/events/sendable_event_trait.rs index faa66c3a66..4e1b11132f 100644 --- a/crates/desktop/artcraft/src/events/sendable_event_trait.rs +++ b/crates/desktop/artcraft/src/core/events/sendable_event_trait.rs @@ -1,5 +1,5 @@ use log::info; -use crate::events::sendable_event_error::SendableEventError; +use crate::core::events::sendable_event_error::SendableEventError; use serde::Serialize; use tauri::{AppHandle, Emitter}; diff --git a/crates/desktop/artcraft/src/core/mod.rs b/crates/desktop/artcraft/src/core/mod.rs new file mode 100644 index 0000000000..da908b11f7 --- /dev/null +++ b/crates/desktop/artcraft/src/core/mod.rs @@ -0,0 +1,6 @@ +pub mod commands; +pub mod events; +pub mod state; +pub mod threads; +pub mod utils; +pub mod windows; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/state/app_preferences/app_preferences.rs b/crates/desktop/artcraft/src/core/state/app_preferences/app_preferences.rs similarity index 88% rename from crates/desktop/artcraft/src/state/app_preferences/app_preferences.rs rename to crates/desktop/artcraft/src/core/state/app_preferences/app_preferences.rs index 30a2b72910..ccae1d67c6 100644 --- a/crates/desktop/artcraft/src/state/app_preferences/app_preferences.rs +++ b/crates/desktop/artcraft/src/core/state/app_preferences/app_preferences.rs @@ -1,6 +1,6 @@ -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; +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; #[derive(Clone)] pub struct AppPreferences { diff --git a/crates/desktop/artcraft/src/state/app_preferences/app_preferences_manager.rs b/crates/desktop/artcraft/src/core/state/app_preferences/app_preferences_manager.rs similarity index 69% rename from crates/desktop/artcraft/src/state/app_preferences/app_preferences_manager.rs rename to crates/desktop/artcraft/src/core/state/app_preferences/app_preferences_manager.rs index 1e6a626fe0..23282364a9 100644 --- a/crates/desktop/artcraft/src/state/app_preferences/app_preferences_manager.rs +++ b/crates/desktop/artcraft/src/core/state/app_preferences/app_preferences_manager.rs @@ -1,5 +1,5 @@ -use crate::state::app_preferences::app_preferences::AppPreferences; -use crate::state::data_dir::app_data_root::AppDataRoot; +use crate::core::state::app_preferences::app_preferences::AppPreferences; +use crate::core::state::data_dir::app_data_root::AppDataRoot; use memory_store::clone_cell::CloneCell; pub type AppPreferencesManager = CloneCell; diff --git a/crates/desktop/artcraft/src/state/app_preferences/app_preferences_serializable.rs b/crates/desktop/artcraft/src/core/state/app_preferences/app_preferences_serializable.rs similarity index 91% rename from crates/desktop/artcraft/src/state/app_preferences/app_preferences_serializable.rs rename to crates/desktop/artcraft/src/core/state/app_preferences/app_preferences_serializable.rs index 3664a9887b..530029d6af 100644 --- a/crates/desktop/artcraft/src/state/app_preferences/app_preferences_serializable.rs +++ b/crates/desktop/artcraft/src/core/state/app_preferences/app_preferences_serializable.rs @@ -1,6 +1,6 @@ -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 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 errors::AnyhowResult; use serde_derive::{Deserialize, Serialize}; diff --git a/crates/desktop/artcraft/src/state/app_preferences/mod.rs b/crates/desktop/artcraft/src/core/state/app_preferences/mod.rs similarity index 100% rename from crates/desktop/artcraft/src/state/app_preferences/mod.rs rename to crates/desktop/artcraft/src/core/state/app_preferences/mod.rs diff --git a/crates/desktop/artcraft/src/state/app_preferences/preferred_download_directory.rs b/crates/desktop/artcraft/src/core/state/app_preferences/preferred_download_directory.rs similarity index 92% rename from crates/desktop/artcraft/src/state/app_preferences/preferred_download_directory.rs rename to crates/desktop/artcraft/src/core/state/app_preferences/preferred_download_directory.rs index 07bdd4c1eb..359d2706a9 100644 --- a/crates/desktop/artcraft/src/state/app_preferences/preferred_download_directory.rs +++ b/crates/desktop/artcraft/src/core/state/app_preferences/preferred_download_directory.rs @@ -58,8 +58,8 @@ impl Display for SystemDownloadDirectory { #[cfg(test)] mod tests { - use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory; - use crate::state::app_preferences::preferred_download_directory::SystemDownloadDirectory; + use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory; + use crate::core::state::app_preferences::preferred_download_directory::SystemDownloadDirectory; use std::str::FromStr; mod json { diff --git a/crates/desktop/artcraft/src/state/app_startup_time.rs b/crates/desktop/artcraft/src/core/state/app_startup_time.rs similarity index 100% rename from crates/desktop/artcraft/src/state/app_startup_time.rs rename to crates/desktop/artcraft/src/core/state/app_startup_time.rs diff --git a/crates/desktop/artcraft/src/state/data_dir/app_assets_dir.rs b/crates/desktop/artcraft/src/core/state/data_dir/app_assets_dir.rs similarity index 92% rename from crates/desktop/artcraft/src/state/data_dir/app_assets_dir.rs rename to crates/desktop/artcraft/src/core/state/data_dir/app_assets_dir.rs index d3ea6d7ede..154eb799ae 100644 --- a/crates/desktop/artcraft/src/state/data_dir/app_assets_dir.rs +++ b/crates/desktop/artcraft/src/core/state/data_dir/app_assets_dir.rs @@ -1,4 +1,4 @@ -use crate::state::data_dir::trait_data_subdir::DataSubdir; +use crate::core::state::data_dir::trait_data_subdir::DataSubdir; use chrono::{DateTime, Local}; use std::path::{Path, PathBuf}; diff --git a/crates/desktop/artcraft/src/state/data_dir/app_credentials_dir.rs b/crates/desktop/artcraft/src/core/state/data_dir/app_credentials_dir.rs similarity index 94% rename from crates/desktop/artcraft/src/state/data_dir/app_credentials_dir.rs rename to crates/desktop/artcraft/src/core/state/data_dir/app_credentials_dir.rs index 2ec84cba9e..d0d80f9fe6 100644 --- a/crates/desktop/artcraft/src/state/data_dir/app_credentials_dir.rs +++ b/crates/desktop/artcraft/src/core/state/data_dir/app_credentials_dir.rs @@ -1,4 +1,4 @@ -use crate::state::data_dir::trait_data_subdir::DataSubdir; +use crate::core::state::data_dir::trait_data_subdir::DataSubdir; use std::path::{Path, PathBuf}; #[derive(Clone)] diff --git a/crates/desktop/artcraft/src/state/data_dir/app_data_root.rs b/crates/desktop/artcraft/src/core/state/data_dir/app_data_root.rs similarity index 87% rename from crates/desktop/artcraft/src/state/data_dir/app_data_root.rs rename to crates/desktop/artcraft/src/core/state/data_dir/app_data_root.rs index 9aa50ef9ec..48011fbce2 100644 --- a/crates/desktop/artcraft/src/state/data_dir/app_data_root.rs +++ b/crates/desktop/artcraft/src/core/state/data_dir/app_data_root.rs @@ -1,12 +1,12 @@ -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 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 anyhow::anyhow; use directories::UserDirs; use std::path::{Path, PathBuf}; diff --git a/crates/desktop/artcraft/src/state/data_dir/app_downloads_dir.rs b/crates/desktop/artcraft/src/core/state/data_dir/app_downloads_dir.rs similarity index 84% rename from crates/desktop/artcraft/src/state/data_dir/app_downloads_dir.rs rename to crates/desktop/artcraft/src/core/state/data_dir/app_downloads_dir.rs index e272e6fab5..f08250686f 100644 --- a/crates/desktop/artcraft/src/state/data_dir/app_downloads_dir.rs +++ b/crates/desktop/artcraft/src/core/state/data_dir/app_downloads_dir.rs @@ -1,4 +1,4 @@ -use crate::state::data_dir::trait_data_subdir::DataSubdir; +use crate::core::state::data_dir::trait_data_subdir::DataSubdir; use std::path::{Path, PathBuf}; #[derive(Clone)] diff --git a/crates/desktop/artcraft/src/state/data_dir/app_settings_dir.rs b/crates/desktop/artcraft/src/core/state/data_dir/app_settings_dir.rs similarity index 90% rename from crates/desktop/artcraft/src/state/data_dir/app_settings_dir.rs rename to crates/desktop/artcraft/src/core/state/data_dir/app_settings_dir.rs index 45430ab038..e3a48b21ef 100644 --- a/crates/desktop/artcraft/src/state/data_dir/app_settings_dir.rs +++ b/crates/desktop/artcraft/src/core/state/data_dir/app_settings_dir.rs @@ -1,5 +1,5 @@ -use crate::state::app_preferences::app_preferences::AppPreferences; -use crate::state::data_dir::trait_data_subdir::DataSubdir; +use crate::core::state::app_preferences::app_preferences::AppPreferences; +use crate::core::state::data_dir::trait_data_subdir::DataSubdir; use errors::AnyhowResult; use serde::Serialize; use std::fs::OpenOptions; diff --git a/crates/desktop/artcraft/src/state/data_dir/app_weights_dir.rs b/crates/desktop/artcraft/src/core/state/data_dir/app_weights_dir.rs similarity index 84% rename from crates/desktop/artcraft/src/state/data_dir/app_weights_dir.rs rename to crates/desktop/artcraft/src/core/state/data_dir/app_weights_dir.rs index 420df4073e..e670f15d5f 100644 --- a/crates/desktop/artcraft/src/state/data_dir/app_weights_dir.rs +++ b/crates/desktop/artcraft/src/core/state/data_dir/app_weights_dir.rs @@ -1,4 +1,4 @@ -use crate::state::data_dir::trait_data_subdir::DataSubdir; +use crate::core::state::data_dir::trait_data_subdir::DataSubdir; use std::path::{Path, PathBuf}; #[derive(Clone)] diff --git a/crates/desktop/artcraft/src/state/data_dir/mod.rs b/crates/desktop/artcraft/src/core/state/data_dir/mod.rs similarity index 100% rename from crates/desktop/artcraft/src/state/data_dir/mod.rs rename to crates/desktop/artcraft/src/core/state/data_dir/mod.rs diff --git a/crates/desktop/artcraft/src/state/data_dir/temporary_dir.rs b/crates/desktop/artcraft/src/core/state/data_dir/temporary_dir.rs similarity index 90% rename from crates/desktop/artcraft/src/state/data_dir/temporary_dir.rs rename to crates/desktop/artcraft/src/core/state/data_dir/temporary_dir.rs index 2597bec578..4456562e35 100644 --- a/crates/desktop/artcraft/src/state/data_dir/temporary_dir.rs +++ b/crates/desktop/artcraft/src/core/state/data_dir/temporary_dir.rs @@ -1,4 +1,4 @@ -use crate::state::data_dir::trait_data_subdir::DataSubdir; +use crate::core::state::data_dir::trait_data_subdir::DataSubdir; use std::path::{Path, PathBuf}; use tempdir::TempDir; use tempfile::{Builder, NamedTempFile}; @@ -36,7 +36,7 @@ impl TemporaryDir { Ok(tempfile) } - pub fn new_named_temp_file_with_extension(&self, extension: &str) -> anyhow::Result { + pub fn new_named_temp_file_with_extension(&self, extension: &str) -> Result { let extension = if extension.starts_with(".") { extension.to_string() } else { diff --git a/crates/desktop/artcraft/src/state/data_dir/trait_data_subdir.rs b/crates/desktop/artcraft/src/core/state/data_dir/trait_data_subdir.rs similarity index 100% rename from crates/desktop/artcraft/src/state/data_dir/trait_data_subdir.rs rename to crates/desktop/artcraft/src/core/state/data_dir/trait_data_subdir.rs diff --git a/crates/desktop/artcraft/src/state/expanduser.rs b/crates/desktop/artcraft/src/core/state/expanduser.rs similarity index 97% rename from crates/desktop/artcraft/src/state/expanduser.rs rename to crates/desktop/artcraft/src/core/state/expanduser.rs index 788d3c259b..11015b250e 100644 --- a/crates/desktop/artcraft/src/state/expanduser.rs +++ b/crates/desktop/artcraft/src/core/state/expanduser.rs @@ -43,7 +43,7 @@ pub fn home_dir() -> io::Result { #[cfg(test)] mod tests { - use crate::state::expanduser::expanduser; + use crate::core::state::expanduser::expanduser; use std::env; use std::path::PathBuf; diff --git a/crates/desktop/artcraft/src/state/main_window_position.rs b/crates/desktop/artcraft/src/core/state/main_window_position.rs similarity index 94% rename from crates/desktop/artcraft/src/state/main_window_position.rs rename to crates/desktop/artcraft/src/core/state/main_window_position.rs index f171a45e30..ec88b83c17 100644 --- a/crates/desktop/artcraft/src/state/main_window_position.rs +++ b/crates/desktop/artcraft/src/core/state/main_window_position.rs @@ -1,5 +1,5 @@ -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::windows::main_window::constants::MAIN_WINDOW_NAME; +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; diff --git a/crates/desktop/artcraft/src/state/main_window_size.rs b/crates/desktop/artcraft/src/core/state/main_window_size.rs similarity index 91% rename from crates/desktop/artcraft/src/state/main_window_size.rs rename to crates/desktop/artcraft/src/core/state/main_window_size.rs index 129aa9d964..5255f47ee6 100644 --- a/crates/desktop/artcraft/src/state/main_window_size.rs +++ b/crates/desktop/artcraft/src/core/state/main_window_size.rs @@ -1,6 +1,6 @@ -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::utils::best_window_size_heuristic::best_window_size_heuristic; -use crate::windows::main_window::constants::MAIN_WINDOW_NAME; +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 errors::AnyhowResult; use serde_derive::{Deserialize, Serialize}; use std::fs::OpenOptions; diff --git a/crates/desktop/artcraft/src/state/mod.rs b/crates/desktop/artcraft/src/core/state/mod.rs similarity index 68% rename from crates/desktop/artcraft/src/state/mod.rs rename to crates/desktop/artcraft/src/core/state/mod.rs index 98ae6b51c0..beda27a3c3 100644 --- a/crates/desktop/artcraft/src/state/mod.rs +++ b/crates/desktop/artcraft/src/core/state/mod.rs @@ -2,9 +2,6 @@ pub mod app_preferences; pub mod app_startup_time; pub mod data_dir; pub mod expanduser; -pub mod fal; pub mod main_window_position; pub mod main_window_size; -pub mod os_platform; -pub mod sora; -pub mod storyteller; \ No newline at end of file +pub mod os_platform; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/state/os_platform.rs b/crates/desktop/artcraft/src/core/state/os_platform.rs similarity index 100% rename from crates/desktop/artcraft/src/state/os_platform.rs rename to crates/desktop/artcraft/src/core/state/os_platform.rs diff --git a/crates/desktop/artcraft/src/threads/discord_presence_thread.rs b/crates/desktop/artcraft/src/core/threads/discord_presence_thread.rs similarity index 100% rename from crates/desktop/artcraft/src/threads/discord_presence_thread.rs rename to crates/desktop/artcraft/src/core/threads/discord_presence_thread.rs diff --git a/crates/desktop/artcraft/src/threads/main_window_thread/main_window_thread.rs b/crates/desktop/artcraft/src/core/threads/main_window_thread/main_window_thread.rs similarity index 74% rename from crates/desktop/artcraft/src/threads/main_window_thread/main_window_thread.rs rename to crates/desktop/artcraft/src/core/threads/main_window_thread/main_window_thread.rs index f9f258327b..b0431488b7 100644 --- a/crates/desktop/artcraft/src/threads/main_window_thread/main_window_thread.rs +++ b/crates/desktop/artcraft/src/core/threads/main_window_thread/main_window_thread.rs @@ -1,11 +1,11 @@ -use crate::state::app_startup_time::AppStartupTime; -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::main_window_position::MainWindowPosition; -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_position_task::persist_window_position_task; -use crate::threads::main_window_thread::persist_window_resize_task::persist_window_resize_task; +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 errors::AnyhowResult; use log::{error, info}; use memory_store::clone_slot::CloneSlot; diff --git a/crates/desktop/artcraft/src/threads/main_window_thread/mod.rs b/crates/desktop/artcraft/src/core/threads/main_window_thread/mod.rs similarity index 100% rename from crates/desktop/artcraft/src/threads/main_window_thread/mod.rs rename to crates/desktop/artcraft/src/core/threads/main_window_thread/mod.rs diff --git a/crates/desktop/artcraft/src/threads/main_window_thread/persist_storyteller_cookies_task.rs b/crates/desktop/artcraft/src/core/threads/main_window_thread/persist_storyteller_cookies_task.rs similarity index 94% rename from crates/desktop/artcraft/src/threads/main_window_thread/persist_storyteller_cookies_task.rs rename to crates/desktop/artcraft/src/core/threads/main_window_thread/persist_storyteller_cookies_task.rs index 49c5c4bdf0..980e218e50 100644 --- a/crates/desktop/artcraft/src/threads/main_window_thread/persist_storyteller_cookies_task.rs +++ b/crates/desktop/artcraft/src/core/threads/main_window_thread/persist_storyteller_cookies_task.rs @@ -1,6 +1,6 @@ -use crate::state::app_startup_time::AppStartupTime; -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager; +use crate::core::state::app_startup_time::AppStartupTime; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; use anyhow::anyhow; use chrono::TimeDelta; use errors::AnyhowResult; diff --git a/crates/desktop/artcraft/src/threads/main_window_thread/persist_window_position_task.rs b/crates/desktop/artcraft/src/core/threads/main_window_thread/persist_window_position_task.rs similarity index 75% rename from crates/desktop/artcraft/src/threads/main_window_thread/persist_window_position_task.rs rename to crates/desktop/artcraft/src/core/threads/main_window_thread/persist_window_position_task.rs index 7abbe82869..4f67f559d3 100644 --- a/crates/desktop/artcraft/src/threads/main_window_thread/persist_window_position_task.rs +++ b/crates/desktop/artcraft/src/core/threads/main_window_thread/persist_window_position_task.rs @@ -1,7 +1,7 @@ -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::main_window_position::MainWindowPosition; -use crate::state::main_window_size::MainWindowSize; -use crate::utils::best_window_size_heuristic::best_window_size_heuristic; +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; diff --git a/crates/desktop/artcraft/src/threads/main_window_thread/persist_window_resize_task.rs b/crates/desktop/artcraft/src/core/threads/main_window_thread/persist_window_resize_task.rs similarity index 80% rename from crates/desktop/artcraft/src/threads/main_window_thread/persist_window_resize_task.rs rename to crates/desktop/artcraft/src/core/threads/main_window_thread/persist_window_resize_task.rs index 3c842478f6..a64aaab6bb 100644 --- a/crates/desktop/artcraft/src/threads/main_window_thread/persist_window_resize_task.rs +++ b/crates/desktop/artcraft/src/core/threads/main_window_thread/persist_window_resize_task.rs @@ -1,6 +1,6 @@ -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::main_window_size::MainWindowSize; -use crate::utils::best_window_size_heuristic::best_window_size_heuristic; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +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; diff --git a/crates/desktop/artcraft/src/core/threads/mod.rs b/crates/desktop/artcraft/src/core/threads/mod.rs new file mode 100644 index 0000000000..80525f98bb --- /dev/null +++ b/crates/desktop/artcraft/src/core/threads/mod.rs @@ -0,0 +1,2 @@ +pub mod discord_presence_thread; +pub mod main_window_thread; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/utils/best_window_size_heuristic.rs b/crates/desktop/artcraft/src/core/utils/best_window_size_heuristic.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/best_window_size_heuristic.rs rename to crates/desktop/artcraft/src/core/utils/best_window_size_heuristic.rs diff --git a/crates/desktop/artcraft/src/utils/clear_all_webview_cookies.rs b/crates/desktop/artcraft/src/core/utils/clear_all_webview_cookies.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/clear_all_webview_cookies.rs rename to crates/desktop/artcraft/src/core/utils/clear_all_webview_cookies.rs diff --git a/crates/desktop/artcraft/src/core/utils/download_media_file_to_temp_dir.rs b/crates/desktop/artcraft/src/core/utils/download_media_file_to_temp_dir.rs new file mode 100644 index 0000000000..611f88affa --- /dev/null +++ b/crates/desktop/artcraft/src/core/utils/download_media_file_to_temp_dir.rs @@ -0,0 +1,29 @@ +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 { + 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 +} diff --git a/crates/desktop/artcraft/src/core/utils/download_url_to_temp_dir.rs b/crates/desktop/artcraft/src/core/utils/download_url_to_temp_dir.rs new file mode 100644 index 0000000000..60122a08d5 --- /dev/null +++ b/crates/desktop/artcraft/src/core/utils/download_url_to_temp_dir.rs @@ -0,0 +1,20 @@ +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 { + 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) +} diff --git a/crates/desktop/artcraft/src/utils/get_url_file_extension.rs b/crates/desktop/artcraft/src/core/utils/get_url_file_extension.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/get_url_file_extension.rs rename to crates/desktop/artcraft/src/core/utils/get_url_file_extension.rs diff --git a/crates/desktop/artcraft/src/utils/image/decode_base64_image.rs b/crates/desktop/artcraft/src/core/utils/image/decode_base64_image.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/image/decode_base64_image.rs rename to crates/desktop/artcraft/src/core/utils/image/decode_base64_image.rs diff --git a/crates/desktop/artcraft/src/utils/image/encode_dynamic_image_base64_png.rs b/crates/desktop/artcraft/src/core/utils/image/encode_dynamic_image_base64_png.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/image/encode_dynamic_image_base64_png.rs rename to crates/desktop/artcraft/src/core/utils/image/encode_dynamic_image_base64_png.rs diff --git a/crates/desktop/artcraft/src/utils/image/encode_rgb_image_base64_png.rs b/crates/desktop/artcraft/src/core/utils/image/encode_rgb_image_base64_png.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/image/encode_rgb_image_base64_png.rs rename to crates/desktop/artcraft/src/core/utils/image/encode_rgb_image_base64_png.rs diff --git a/crates/desktop/artcraft/src/utils/image/mod.rs b/crates/desktop/artcraft/src/core/utils/image/mod.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/image/mod.rs rename to crates/desktop/artcraft/src/core/utils/image/mod.rs diff --git a/crates/desktop/artcraft/src/utils/large_file_transfer/download.rs b/crates/desktop/artcraft/src/core/utils/large_file_transfer/download.rs similarity index 97% rename from crates/desktop/artcraft/src/utils/large_file_transfer/download.rs rename to crates/desktop/artcraft/src/core/utils/large_file_transfer/download.rs index 2bd250d025..7b2130a0fa 100644 --- a/crates/desktop/artcraft/src/utils/large_file_transfer/download.rs +++ b/crates/desktop/artcraft/src/core/utils/large_file_transfer/download.rs @@ -1,7 +1,7 @@ //! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs -use crate::utils::large_file_transfer::error::Error; -use crate::utils::large_file_transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME}; +use crate::core::utils::large_file_transfer::error::Error; +use crate::core::utils::large_file_transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME}; use futures::stream::FuturesUnordered; use futures::StreamExt; use log::info; diff --git a/crates/desktop/artcraft/src/utils/large_file_transfer/error.rs b/crates/desktop/artcraft/src/core/utils/large_file_transfer/error.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/large_file_transfer/error.rs rename to crates/desktop/artcraft/src/core/utils/large_file_transfer/error.rs diff --git a/crates/desktop/artcraft/src/utils/large_file_transfer/mod.rs b/crates/desktop/artcraft/src/core/utils/large_file_transfer/mod.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/large_file_transfer/mod.rs rename to crates/desktop/artcraft/src/core/utils/large_file_transfer/mod.rs diff --git a/crates/desktop/artcraft/src/utils/large_file_transfer/upload.rs b/crates/desktop/artcraft/src/core/utils/large_file_transfer/upload.rs similarity index 97% rename from crates/desktop/artcraft/src/utils/large_file_transfer/upload.rs rename to crates/desktop/artcraft/src/core/utils/large_file_transfer/upload.rs index 3523a7747f..fc70ceb4f3 100644 --- a/crates/desktop/artcraft/src/utils/large_file_transfer/upload.rs +++ b/crates/desktop/artcraft/src/core/utils/large_file_transfer/upload.rs @@ -1,7 +1,7 @@ //! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs -use crate::utils::large_file_transfer::error::Error; -use crate::utils::large_file_transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME}; +use crate::core::utils::large_file_transfer::error::Error; +use crate::core::utils::large_file_transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME}; use futures::stream::FuturesUnordered; use futures::StreamExt; use reqwest::header::CONTENT_LENGTH; diff --git a/crates/desktop/artcraft/src/utils/large_file_transfer/util.rs b/crates/desktop/artcraft/src/core/utils/large_file_transfer/util.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/large_file_transfer/util.rs rename to crates/desktop/artcraft/src/core/utils/large_file_transfer/util.rs diff --git a/crates/desktop/artcraft/src/utils/mod.rs b/crates/desktop/artcraft/src/core/utils/mod.rs similarity index 69% rename from crates/desktop/artcraft/src/utils/mod.rs rename to crates/desktop/artcraft/src/core/utils/mod.rs index 279b6badb4..d9003fc855 100644 --- a/crates/desktop/artcraft/src/utils/mod.rs +++ b/crates/desktop/artcraft/src/core/utils/mod.rs @@ -1,10 +1,12 @@ 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 sora; pub mod webview_unsafe; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/utils/object_store.rs b/crates/desktop/artcraft/src/core/utils/object_store.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/object_store.rs rename to crates/desktop/artcraft/src/core/utils/object_store.rs diff --git a/crates/desktop/artcraft/src/core/utils/save_base64_image_to_temp_dir.rs b/crates/desktop/artcraft/src/core/utils/save_base64_image_to_temp_dir.rs new file mode 100644 index 0000000000..4dcb759f2f --- /dev/null +++ b/crates/desktop/artcraft/src/core/utils/save_base64_image_to_temp_dir.rs @@ -0,0 +1,25 @@ +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 { + 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 +} diff --git a/crates/desktop/artcraft/src/utils/simple_http_download.rs b/crates/desktop/artcraft/src/core/utils/simple_http_download.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/simple_http_download.rs rename to crates/desktop/artcraft/src/core/utils/simple_http_download.rs diff --git a/crates/desktop/artcraft/src/utils/simple_http_download_to_tempfile.rs b/crates/desktop/artcraft/src/core/utils/simple_http_download_to_tempfile.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/simple_http_download_to_tempfile.rs rename to crates/desktop/artcraft/src/core/utils/simple_http_download_to_tempfile.rs diff --git a/crates/desktop/artcraft/src/utils/webview_unsafe.rs b/crates/desktop/artcraft/src/core/utils/webview_unsafe.rs similarity index 100% rename from crates/desktop/artcraft/src/utils/webview_unsafe.rs rename to crates/desktop/artcraft/src/core/utils/webview_unsafe.rs diff --git a/crates/desktop/artcraft/src/windows/main_window/constants.rs b/crates/desktop/artcraft/src/core/windows/main_window/constants.rs similarity index 100% rename from crates/desktop/artcraft/src/windows/main_window/constants.rs rename to crates/desktop/artcraft/src/core/windows/main_window/constants.rs diff --git a/crates/desktop/artcraft/src/windows/main_window/mod.rs b/crates/desktop/artcraft/src/core/windows/main_window/mod.rs similarity index 100% rename from crates/desktop/artcraft/src/windows/main_window/mod.rs rename to crates/desktop/artcraft/src/core/windows/main_window/mod.rs diff --git a/crates/desktop/artcraft/src/core/windows/mod.rs b/crates/desktop/artcraft/src/core/windows/mod.rs new file mode 100644 index 0000000000..84cc560f4b --- /dev/null +++ b/crates/desktop/artcraft/src/core/windows/mod.rs @@ -0,0 +1 @@ +pub mod main_window; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/lib.rs b/crates/desktop/artcraft/src/lib.rs index 82e491d46e..fccba3bd01 100644 --- a/crates/desktop/artcraft/src/lib.rs +++ b/crates/desktop/artcraft/src/lib.rs @@ -1,36 +1,36 @@ -pub mod commands; -pub mod events; -pub mod state; -pub mod threads; -pub mod utils; -pub mod windows; +pub mod core; +pub mod services; use tauri::Manager; -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::fal::fal_background_removal_command::fal_background_removal_command; -use crate::commands::fal::get_fal_api_key_command::get_fal_api_key_command; -use crate::commands::fal::set_fal_api_key_command::set_fal_api_key_command; -use crate::commands::flip_image::flip_image; -use crate::commands::platform_info_command::platform_info_command; -use crate::commands::sora::check_sora_session_command::check_sora_session_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::commands::sora::sora_logout_command::sora_logout_command; -use crate::state::app_preferences::app_preferences_manager::load_app_preferences_or_default; -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::fal::fal_credential_manager::FalCredentialManager; -use crate::state::main_window_position::MainWindowPosition; -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_task_polling_thread::sora_task_polling_thread; -use crate::utils::webview_unsafe::webview_unsafe_for_app; +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 tauri_plugin_dialog; use tauri_plugin_http; @@ -59,11 +59,15 @@ pub fn run() { 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() @@ -97,13 +101,19 @@ 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) => {} @@ -136,18 +146,22 @@ pub fn run() { 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(sora_creds_manager) .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, diff --git a/crates/desktop/artcraft/src/commands/fal/fal_background_removal_command.rs b/crates/desktop/artcraft/src/services/fal/commands/fal_background_removal_command.rs similarity index 70% rename from crates/desktop/artcraft/src/commands/fal/fal_background_removal_command.rs rename to crates/desktop/artcraft/src/services/fal/commands/fal_background_removal_command.rs index 076930642b..265e45a0a2 100644 --- a/crates/desktop/artcraft/src/commands/fal/fal_background_removal_command.rs +++ b/crates/desktop/artcraft/src/services/fal/commands/fal_background_removal_command.rs @@ -1,24 +1,27 @@ -use crate::commands::command_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus, CommandResult, SerializeMarker}; -use crate::events::sendable_event_trait::SendableEvent; -use crate::events::sora::sora_image_enqueue_failure_event::SoraImageEnqueueFailureEvent; -use crate::events::sora::sora_image_enqueue_success_event::SoraImageEnqueueSuccessEvent; -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::data_dir::trait_data_subdir::DataSubdir; -use crate::state::fal::fal_credential_manager::FalCredentialManager; -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::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager; -use crate::utils::get_url_file_extension::get_url_file_extension; -use crate::utils::simple_http_download::simple_http_download; -use crate::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile; +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}; @@ -40,14 +43,13 @@ use std::io::{Cursor, Read, Write}; use std::ops::Add; use std::path::PathBuf; use std::time::Duration; -use storyteller_client::api_error::ApiError; -use storyteller_client::api_error::ApiError::InternalServerError; +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 filesys::file_read_bytes::file_read_bytes; use tokens::tokens::media_files::MediaFileToken; #[derive(Deserialize)] @@ -73,7 +75,7 @@ impl SerializeMarker for FalBackgroundRemovalSuccessResponse {} #[derive(Serialize, Debug)] #[serde(rename_all = "snake_case")] -pub enum SoraImageRemixErrorType { +pub enum BackgroundRemovalErrorType { /// Generic server error ServerError, /// No Fal API key available @@ -87,7 +89,7 @@ pub async fn fal_background_removal_command( app_data_root: State<'_, AppDataRoot>, fal_creds_manager: State<'_, FalCredentialManager>, storyteller_creds_manager: State<'_, StorytellerCredentialManager>, -) -> CommandResult { +) -> CommandResult { info!("fal_background_removal_command called; image media token: {:?}", request.image_media_token); @@ -100,7 +102,7 @@ pub async fn fal_background_removal_command( return Err(CommandErrorResponseWrapper { status: CommandErrorStatus::Unauthorized, error_message: Some("You need to set a FAL api key".to_string()), - error_type: Some(SoraImageRemixErrorType::NeedsFalApiKey), + error_type: Some(BackgroundRemovalErrorType::NeedsFalApiKey), error_details: None, }); } @@ -123,7 +125,7 @@ pub async fn fal_background_removal_command( //} let mut status = CommandErrorStatus::ServerError; - let mut error_type = SoraImageRemixErrorType::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) { @@ -143,7 +145,7 @@ pub async fn fal_background_removal_command( //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, @@ -159,7 +161,6 @@ enum InnerError { AnyhowError(AnyhowError), StorytellerApiError(ApiError), DecodeError(DecodeError), - //ImageError(image::ImageError), IoError(std::io::Error), } @@ -187,12 +188,6 @@ impl From for InnerError { } } -//impl From for InnerError { -// fn from(value: image::ImageError) -> Self { -// Self::ImageError(value) -// } -//} -// impl From for InnerError { fn from(value: std::io::Error) -> Self { Self::IoError(value) @@ -212,10 +207,10 @@ pub async fn remove_background( let mut temp_download; if let Some(media_token) = request.image_media_token { - temp_download = download_media_file(&app_data_root, &media_token).await?; + 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 = persist_base64(&app_data_root, base64_bytes).await?; + 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"))); } @@ -260,37 +255,3 @@ pub async fn remove_background( Ok((response, base64_bytes)) } -async fn download_media_file(app_data_root: &AppDataRoot, token: &MediaFileToken) -> Result { - 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()); - - //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 -} - -async fn persist_base64(app_data_root: &AppDataRoot, base64_image: String) -> Result { - 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 -} diff --git a/crates/desktop/artcraft/src/services/fal/commands/fal_hunyuan_image_to_3d_command.rs b/crates/desktop/artcraft/src/services/fal/commands/fal_hunyuan_image_to_3d_command.rs new file mode 100644 index 0000000000..c3418f664e --- /dev/null +++ b/crates/desktop/artcraft/src/services/fal/commands/fal_hunyuan_image_to_3d_command.rs @@ -0,0 +1,220 @@ +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, + + /// Base64-encoded image + pub base64_image: Option, +} + +#[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 for InnerError { + fn from(value: AnyhowError) -> Self { + Self::AnyhowError(value) + } +} + +impl From for InnerError { + fn from(value: FalErrorPlus) -> Self { + Self::FalError(value) + } +} + +impl From for InnerError { + fn from(value: ApiError) -> Self { + Self::StorytellerApiError(value) + } +} + +impl From for InnerError { + fn from(value: DecodeError) -> Self { + Self::DecodeError(value) + } +} + +impl From 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(()) +} diff --git a/crates/desktop/artcraft/src/services/fal/commands/fal_kling_image_to_video_command.rs b/crates/desktop/artcraft/src/services/fal/commands/fal_kling_image_to_video_command.rs new file mode 100644 index 0000000000..3ebf5f4406 --- /dev/null +++ b/crates/desktop/artcraft/src/services/fal/commands/fal_kling_image_to_video_command.rs @@ -0,0 +1,220 @@ +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, + + /// Base64-encoded image + pub base64_image: Option, +} + +#[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 for InnerError { + fn from(value: AnyhowError) -> Self { + Self::AnyhowError(value) + } +} + +impl From for InnerError { + fn from(value: FalErrorPlus) -> Self { + Self::FalError(value) + } +} + +impl From for InnerError { + fn from(value: ApiError) -> Self { + Self::StorytellerApiError(value) + } +} + +impl From for InnerError { + fn from(value: DecodeError) -> Self { + Self::DecodeError(value) + } +} + +impl From 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(()) +} diff --git a/crates/desktop/artcraft/src/commands/fal/get_fal_api_key_command.rs b/crates/desktop/artcraft/src/services/fal/commands/get_fal_api_key_command.rs similarity index 72% rename from crates/desktop/artcraft/src/commands/fal/get_fal_api_key_command.rs rename to crates/desktop/artcraft/src/services/fal/commands/get_fal_api_key_command.rs index 4f7147cc7c..32aca9cc25 100644 --- a/crates/desktop/artcraft/src/commands/fal/get_fal_api_key_command.rs +++ b/crates/desktop/artcraft/src/services/fal/commands/get_fal_api_key_command.rs @@ -1,8 +1,8 @@ -use crate::commands::command_response_wrapper::{CommandResult, SerializeMarker}; -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 crate::state::fal::fal_credential_manager::FalCredentialManager; +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; diff --git a/crates/desktop/artcraft/src/commands/fal/mod.rs b/crates/desktop/artcraft/src/services/fal/commands/mod.rs similarity index 55% rename from crates/desktop/artcraft/src/commands/fal/mod.rs rename to crates/desktop/artcraft/src/services/fal/commands/mod.rs index c69b86c32b..49f725d47b 100644 --- a/crates/desktop/artcraft/src/commands/fal/mod.rs +++ b/crates/desktop/artcraft/src/services/fal/commands/mod.rs @@ -1,3 +1,5 @@ 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; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/commands/fal/set_fal_api_key_command.rs b/crates/desktop/artcraft/src/services/fal/commands/set_fal_api_key_command.rs similarity index 76% rename from crates/desktop/artcraft/src/commands/fal/set_fal_api_key_command.rs rename to crates/desktop/artcraft/src/services/fal/commands/set_fal_api_key_command.rs index 976a774307..9504bde14a 100644 --- a/crates/desktop/artcraft/src/commands/fal/set_fal_api_key_command.rs +++ b/crates/desktop/artcraft/src/services/fal/commands/set_fal_api_key_command.rs @@ -1,8 +1,8 @@ -use crate::commands::command_response_wrapper::CommandResult; -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 crate::state::fal::fal_credential_manager::FalCredentialManager; +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; diff --git a/crates/desktop/artcraft/src/services/fal/mod.rs b/crates/desktop/artcraft/src/services/fal/mod.rs new file mode 100644 index 0000000000..2e33656fb0 --- /dev/null +++ b/crates/desktop/artcraft/src/services/fal/mod.rs @@ -0,0 +1,3 @@ +pub mod commands; +pub mod state; +pub mod threads; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/state/fal/fal_credential_manager.rs b/crates/desktop/artcraft/src/services/fal/state/fal_credential_manager.rs similarity index 94% rename from crates/desktop/artcraft/src/state/fal/fal_credential_manager.rs rename to crates/desktop/artcraft/src/services/fal/state/fal_credential_manager.rs index 95ba34c74b..244e375328 100644 --- a/crates/desktop/artcraft/src/state/fal/fal_credential_manager.rs +++ b/crates/desktop/artcraft/src/services/fal/state/fal_credential_manager.rs @@ -1,5 +1,5 @@ -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::fal::read_fal_api_key_from_disk::read_fal_api_key_from_disk; +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; diff --git a/crates/desktop/artcraft/src/services/fal/state/fal_task_queue.rs b/crates/desktop/artcraft/src/services/fal/state/fal_task_queue.rs new file mode 100644 index 0000000000..e733c886e1 --- /dev/null +++ b/crates/desktop/artcraft/src/services/fal/state/fal_task_queue.rs @@ -0,0 +1,164 @@ +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, + 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>> +} + +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 { + 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 { + 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> { + 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 { + 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> { + 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> { + 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> { + 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> { + 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(()) + } + } + } +} diff --git a/crates/desktop/artcraft/src/state/fal/mod.rs b/crates/desktop/artcraft/src/services/fal/state/mod.rs similarity index 73% rename from crates/desktop/artcraft/src/state/fal/mod.rs rename to crates/desktop/artcraft/src/services/fal/state/mod.rs index fd55133c18..f9f5484671 100644 --- a/crates/desktop/artcraft/src/state/fal/mod.rs +++ b/crates/desktop/artcraft/src/services/fal/state/mod.rs @@ -1,2 +1,3 @@ pub mod fal_credential_manager; +pub mod fal_task_queue; pub mod read_fal_api_key_from_disk; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/state/fal/read_fal_api_key_from_disk.rs b/crates/desktop/artcraft/src/services/fal/state/read_fal_api_key_from_disk.rs similarity index 89% rename from crates/desktop/artcraft/src/state/fal/read_fal_api_key_from_disk.rs rename to crates/desktop/artcraft/src/services/fal/state/read_fal_api_key_from_disk.rs index e19ede95c0..57be2cfbfa 100644 --- a/crates/desktop/artcraft/src/state/fal/read_fal_api_key_from_disk.rs +++ b/crates/desktop/artcraft/src/services/fal/state/read_fal_api_key_from_disk.rs @@ -1,4 +1,4 @@ -use crate::state::data_dir::app_data_root::AppDataRoot; +use crate::core::state::data_dir::app_data_root::AppDataRoot; use anyhow::anyhow; use errors::AnyhowResult; use fal_client::creds::fal_api_key::FalApiKey; diff --git a/crates/desktop/artcraft/src/services/fal/threads/fal_task_polling_thread.rs b/crates/desktop/artcraft/src/services/fal/threads/fal_task_polling_thread.rs new file mode 100644 index 0000000000..bf8f1f4883 --- /dev/null +++ b/crates/desktop/artcraft/src/services/fal/threads/fal_task_polling_thread.rs @@ -0,0 +1,234 @@ +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::>(); + + fal_task_queue.remove_list(&succeeded_task_ids)?; + + tokio::time::sleep(std::time::Duration::from_millis(6_000)).await; + } +} + diff --git a/crates/desktop/artcraft/src/services/fal/threads/mod.rs b/crates/desktop/artcraft/src/services/fal/threads/mod.rs new file mode 100644 index 0000000000..725a68337f --- /dev/null +++ b/crates/desktop/artcraft/src/services/fal/threads/mod.rs @@ -0,0 +1 @@ +pub mod fal_task_polling_thread; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/mod.rs b/crates/desktop/artcraft/src/services/mod.rs new file mode 100644 index 0000000000..8b92875ecb --- /dev/null +++ b/crates/desktop/artcraft/src/services/mod.rs @@ -0,0 +1,3 @@ +pub mod fal; +pub mod sora; +pub mod storyteller; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/commands/sora/check_sora_session_command.rs b/crates/desktop/artcraft/src/services/sora/commands/check_sora_session_command.rs similarity index 87% rename from crates/desktop/artcraft/src/commands/sora/check_sora_session_command.rs rename to crates/desktop/artcraft/src/services/sora/commands/check_sora_session_command.rs index 35aea32e98..afe9bad6e7 100644 --- a/crates/desktop/artcraft/src/commands/sora/check_sora_session_command.rs +++ b/crates/desktop/artcraft/src/services/sora/commands/check_sora_session_command.rs @@ -1,10 +1,9 @@ -use std::ops::Add; +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 crate::commands::app_preferences::get_app_preferences_command::AppPreferencesPayload; -use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager; -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::sora::sora_credential_manager::SoraCredentialManager; -use crate::windows::sora_login_window::open_sora_login_window::open_sora_login_window; use errors::AnyhowResult; use log::{error, info}; use once_cell::sync::Lazy; @@ -12,6 +11,7 @@ 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 diff --git a/crates/desktop/artcraft/src/commands/sora/mod.rs b/crates/desktop/artcraft/src/services/sora/commands/mod.rs similarity index 100% rename from crates/desktop/artcraft/src/commands/sora/mod.rs rename to crates/desktop/artcraft/src/services/sora/commands/mod.rs diff --git a/crates/desktop/artcraft/src/commands/sora/open_sora_login_command.rs b/crates/desktop/artcraft/src/services/sora/commands/open_sora_login_command.rs similarity index 71% rename from crates/desktop/artcraft/src/commands/sora/open_sora_login_command.rs rename to crates/desktop/artcraft/src/services/sora/commands/open_sora_login_command.rs index d60ea4192b..1fe9316704 100644 --- a/crates/desktop/artcraft/src/commands/sora/open_sora_login_command.rs +++ b/crates/desktop/artcraft/src/services/sora/commands/open_sora_login_command.rs @@ -1,7 +1,7 @@ -use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager; -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::sora::sora_credential_manager::SoraCredentialManager; -use crate::windows::sora_login_window::open_sora_login_window::open_sora_login_window; +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; diff --git a/crates/desktop/artcraft/src/commands/sora/sora_image_generation_command.rs b/crates/desktop/artcraft/src/services/sora/commands/sora_image_generation_command.rs similarity index 92% rename from crates/desktop/artcraft/src/commands/sora/sora_image_generation_command.rs rename to crates/desktop/artcraft/src/services/sora/commands/sora_image_generation_command.rs index 1eeea144bc..9684f1ad57 100644 --- a/crates/desktop/artcraft/src/commands/sora/sora_image_generation_command.rs +++ b/crates/desktop/artcraft/src/services/sora/commands/sora_image_generation_command.rs @@ -1,6 +1,6 @@ -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::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 base64::prelude::BASE64_STANDARD; use base64::Engine; use errors::AnyhowResult; diff --git a/crates/desktop/artcraft/src/commands/sora/sora_image_remix_command.rs b/crates/desktop/artcraft/src/services/sora/commands/sora_image_remix_command.rs similarity index 89% rename from crates/desktop/artcraft/src/commands/sora/sora_image_remix_command.rs rename to crates/desktop/artcraft/src/services/sora/commands/sora_image_remix_command.rs index f86eae9e33..6ccb34bd89 100644 --- a/crates/desktop/artcraft/src/commands/sora/sora_image_remix_command.rs +++ b/crates/desktop/artcraft/src/services/sora/commands/sora_image_remix_command.rs @@ -1,15 +1,15 @@ -use crate::commands::command_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus, CommandResult}; -use crate::events::sendable_event_trait::SendableEvent; -use crate::events::sora::sora_image_enqueue_failure_event::SoraImageEnqueueFailureEvent; -use crate::events::sora::sora_image_enqueue_success_event::SoraImageEnqueueSuccessEvent; -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 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}; @@ -31,8 +31,8 @@ use std::fs::read_to_string; use std::io::Cursor; use std::ops::Add; use std::time::Duration; -use storyteller_client::api_error::ApiError; -use storyteller_client::api_error::ApiError::InternalServerError; +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}; diff --git a/crates/desktop/artcraft/src/commands/sora/sora_logout_command.rs b/crates/desktop/artcraft/src/services/sora/commands/sora_logout_command.rs similarity index 68% rename from crates/desktop/artcraft/src/commands/sora/sora_logout_command.rs rename to crates/desktop/artcraft/src/services/sora/commands/sora_logout_command.rs index 531fee02f6..40231db0e5 100644 --- a/crates/desktop/artcraft/src/commands/sora/sora_logout_command.rs +++ b/crates/desktop/artcraft/src/services/sora/commands/sora_logout_command.rs @@ -1,9 +1,9 @@ -use crate::commands::app_preferences::get_app_preferences_command::AppPreferencesPayload; -use crate::commands::command_response_wrapper::{CommandResult, CommandSuccessResponseWrapper, SimpleCommandResult}; -use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager; -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::sora::sora_credential_manager::SoraCredentialManager; -use crate::windows::sora_login_window::open_sora_login_window::open_sora_login_window; +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; diff --git a/crates/desktop/artcraft/src/events/sora/mod.rs b/crates/desktop/artcraft/src/services/sora/events/mod.rs similarity index 100% rename from crates/desktop/artcraft/src/events/sora/mod.rs rename to crates/desktop/artcraft/src/services/sora/events/mod.rs diff --git a/crates/desktop/artcraft/src/events/sora/sora_image_enqueue_failure_event.rs b/crates/desktop/artcraft/src/services/sora/events/sora_image_enqueue_failure_event.rs similarity index 80% rename from crates/desktop/artcraft/src/events/sora/sora_image_enqueue_failure_event.rs rename to crates/desktop/artcraft/src/services/sora/events/sora_image_enqueue_failure_event.rs index c8317b088b..fec2a81cd6 100644 --- a/crates/desktop/artcraft/src/events/sora/sora_image_enqueue_failure_event.rs +++ b/crates/desktop/artcraft/src/services/sora/events/sora_image_enqueue_failure_event.rs @@ -1,4 +1,4 @@ -use crate::events::sendable_event_trait::SendableEvent; +use crate::core::events::sendable_event_trait::SendableEvent; use serde_derive::Serialize; #[derive(Debug, Clone, Serialize)] diff --git a/crates/desktop/artcraft/src/events/sora/sora_image_enqueue_success_event.rs b/crates/desktop/artcraft/src/services/sora/events/sora_image_enqueue_success_event.rs similarity index 80% rename from crates/desktop/artcraft/src/events/sora/sora_image_enqueue_success_event.rs rename to crates/desktop/artcraft/src/services/sora/events/sora_image_enqueue_success_event.rs index 7d3a8b6a9d..1a5c67e9ee 100644 --- a/crates/desktop/artcraft/src/events/sora/sora_image_enqueue_success_event.rs +++ b/crates/desktop/artcraft/src/services/sora/events/sora_image_enqueue_success_event.rs @@ -1,4 +1,4 @@ -use crate::events::sendable_event_trait::SendableEvent; +use crate::core::events::sendable_event_trait::SendableEvent; use serde_derive::Serialize; #[derive(Debug, Clone, Serialize)] diff --git a/crates/desktop/artcraft/src/events/sora/sora_image_generation_complete_event.rs b/crates/desktop/artcraft/src/services/sora/events/sora_image_generation_complete_event.rs similarity index 84% rename from crates/desktop/artcraft/src/events/sora/sora_image_generation_complete_event.rs rename to crates/desktop/artcraft/src/services/sora/events/sora_image_generation_complete_event.rs index 09cddf624d..0286116814 100644 --- a/crates/desktop/artcraft/src/events/sora/sora_image_generation_complete_event.rs +++ b/crates/desktop/artcraft/src/services/sora/events/sora_image_generation_complete_event.rs @@ -1,6 +1,6 @@ use serde_derive::Serialize; use tokens::tokens::media_files::MediaFileToken; -use crate::events::sendable_event_trait::SendableEvent; +use crate::core::events::sendable_event_trait::SendableEvent; #[derive(Debug, Clone, Serialize)] pub struct SoraImageGenerationCompleteEvent { diff --git a/crates/desktop/artcraft/src/events/sora/sora_image_generation_failed_event.rs b/crates/desktop/artcraft/src/services/sora/events/sora_image_generation_failed_event.rs similarity index 81% rename from crates/desktop/artcraft/src/events/sora/sora_image_generation_failed_event.rs rename to crates/desktop/artcraft/src/services/sora/events/sora_image_generation_failed_event.rs index 6a4a20f79a..1f788edba7 100644 --- a/crates/desktop/artcraft/src/events/sora/sora_image_generation_failed_event.rs +++ b/crates/desktop/artcraft/src/services/sora/events/sora_image_generation_failed_event.rs @@ -1,5 +1,5 @@ use serde_derive::Serialize; -use crate::events::sendable_event_trait::SendableEvent; +use crate::core::events::sendable_event_trait::SendableEvent; #[derive(Debug, Clone, Serialize)] pub struct SoraImageGenerationFailedEvent { diff --git a/crates/desktop/artcraft/src/events/sora/sora_login_success_event.rs b/crates/desktop/artcraft/src/services/sora/events/sora_login_success_event.rs similarity index 79% rename from crates/desktop/artcraft/src/events/sora/sora_login_success_event.rs rename to crates/desktop/artcraft/src/services/sora/events/sora_login_success_event.rs index b64f70115c..cd09847a48 100644 --- a/crates/desktop/artcraft/src/events/sora/sora_login_success_event.rs +++ b/crates/desktop/artcraft/src/services/sora/events/sora_login_success_event.rs @@ -1,4 +1,4 @@ -use crate::events::sendable_event_trait::SendableEvent; +use crate::core::events::sendable_event_trait::SendableEvent; use serde_derive::Serialize; #[derive(Debug, Clone, Serialize)] diff --git a/crates/desktop/artcraft/src/services/sora/mod.rs b/crates/desktop/artcraft/src/services/sora/mod.rs new file mode 100644 index 0000000000..c352ff453a --- /dev/null +++ b/crates/desktop/artcraft/src/services/sora/mod.rs @@ -0,0 +1,5 @@ +pub mod commands; +pub mod events; +pub mod state; +pub mod threads; +pub mod windows; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/utils/sora/initialize_sora_jwt_bearer_token.rs b/crates/desktop/artcraft/src/services/sora/state/initialize_sora_jwt_bearer_token.rs similarity index 93% rename from crates/desktop/artcraft/src/utils/sora/initialize_sora_jwt_bearer_token.rs rename to crates/desktop/artcraft/src/services/sora/state/initialize_sora_jwt_bearer_token.rs index 307b48d6b7..9830e87b8b 100644 --- a/crates/desktop/artcraft/src/utils/sora/initialize_sora_jwt_bearer_token.rs +++ b/crates/desktop/artcraft/src/services/sora/state/initialize_sora_jwt_bearer_token.rs @@ -1,4 +1,4 @@ -use crate::state::data_dir::app_data_root::AppDataRoot; +use crate::core::state::data_dir::app_data_root::AppDataRoot; use anyhow::anyhow; use errors::AnyhowResult; use log::info; diff --git a/crates/desktop/artcraft/src/state/sora/mod.rs b/crates/desktop/artcraft/src/services/sora/state/mod.rs similarity index 83% rename from crates/desktop/artcraft/src/state/sora/mod.rs rename to crates/desktop/artcraft/src/services/sora/state/mod.rs index 5a0e3cc740..ddc54a9f97 100644 --- a/crates/desktop/artcraft/src/state/sora/mod.rs +++ b/crates/desktop/artcraft/src/services/sora/state/mod.rs @@ -1,3 +1,4 @@ +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; diff --git a/crates/desktop/artcraft/src/state/sora/read_sora_credentials_from_disk.rs b/crates/desktop/artcraft/src/services/sora/state/read_sora_credentials_from_disk.rs similarity index 95% rename from crates/desktop/artcraft/src/state/sora/read_sora_credentials_from_disk.rs rename to crates/desktop/artcraft/src/services/sora/state/read_sora_credentials_from_disk.rs index ac373220c0..be37700346 100644 --- a/crates/desktop/artcraft/src/state/sora/read_sora_credentials_from_disk.rs +++ b/crates/desktop/artcraft/src/services/sora/state/read_sora_credentials_from_disk.rs @@ -1,4 +1,4 @@ -use crate::state::data_dir::app_data_root::AppDataRoot; +use crate::core::state::data_dir::app_data_root::AppDataRoot; use anyhow::anyhow; use errors::AnyhowResult; use openai_sora_client::creds::sora_cookies::SoraCookies; diff --git a/crates/desktop/artcraft/src/state/sora/read_sora_credentials_from_disk_with_upgrade.rs b/crates/desktop/artcraft/src/services/sora/state/read_sora_credentials_from_disk_with_upgrade.rs similarity index 97% rename from crates/desktop/artcraft/src/state/sora/read_sora_credentials_from_disk_with_upgrade.rs rename to crates/desktop/artcraft/src/services/sora/state/read_sora_credentials_from_disk_with_upgrade.rs index 8f52abc008..14bf06ab95 100644 --- a/crates/desktop/artcraft/src/state/sora/read_sora_credentials_from_disk_with_upgrade.rs +++ b/crates/desktop/artcraft/src/services/sora/state/read_sora_credentials_from_disk_with_upgrade.rs @@ -1,4 +1,4 @@ -use crate::state::data_dir::app_data_root::AppDataRoot; +use crate::core::state::data_dir::app_data_root::AppDataRoot; use anyhow::anyhow; use errors::AnyhowResult; use log::{info, warn}; diff --git a/crates/desktop/artcraft/src/state/sora/sora_credential_holder.rs b/crates/desktop/artcraft/src/services/sora/state/sora_credential_holder.rs similarity index 100% rename from crates/desktop/artcraft/src/state/sora/sora_credential_holder.rs rename to crates/desktop/artcraft/src/services/sora/state/sora_credential_holder.rs diff --git a/crates/desktop/artcraft/src/state/sora/sora_credential_manager.rs b/crates/desktop/artcraft/src/services/sora/state/sora_credential_manager.rs similarity index 95% rename from crates/desktop/artcraft/src/state/sora/sora_credential_manager.rs rename to crates/desktop/artcraft/src/services/sora/state/sora_credential_manager.rs index 13b62b9f2a..ad60a02fc0 100644 --- a/crates/desktop/artcraft/src/state/sora/sora_credential_manager.rs +++ b/crates/desktop/artcraft/src/services/sora/state/sora_credential_manager.rs @@ -1,7 +1,7 @@ -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::sora::sora_credential_stats::SoraCredentialStats; +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 anyhow::anyhow; use errors::AnyhowResult; use log::{error, info, warn}; diff --git a/crates/desktop/artcraft/src/state/sora/sora_credential_stats.rs b/crates/desktop/artcraft/src/services/sora/state/sora_credential_stats.rs similarity index 100% rename from crates/desktop/artcraft/src/state/sora/sora_credential_stats.rs rename to crates/desktop/artcraft/src/services/sora/state/sora_credential_stats.rs diff --git a/crates/desktop/artcraft/src/state/sora/sora_task_queue.rs b/crates/desktop/artcraft/src/services/sora/state/sora_task_queue.rs similarity index 98% rename from crates/desktop/artcraft/src/state/sora/sora_task_queue.rs rename to crates/desktop/artcraft/src/services/sora/state/sora_task_queue.rs index 10a5a1c025..1fe8022dc5 100644 --- a/crates/desktop/artcraft/src/state/sora/sora_task_queue.rs +++ b/crates/desktop/artcraft/src/services/sora/state/sora_task_queue.rs @@ -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<()> { diff --git a/crates/desktop/artcraft/src/services/sora/threads/mod.rs b/crates/desktop/artcraft/src/services/sora/threads/mod.rs new file mode 100644 index 0000000000..e55d113fad --- /dev/null +++ b/crates/desktop/artcraft/src/services/sora/threads/mod.rs @@ -0,0 +1 @@ +pub mod sora_task_polling_thread; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/threads/sora_task_polling_thread.rs b/crates/desktop/artcraft/src/services/sora/threads/sora_task_polling_thread.rs similarity index 88% rename from crates/desktop/artcraft/src/threads/sora_task_polling_thread.rs rename to crates/desktop/artcraft/src/services/sora/threads/sora_task_polling_thread.rs index da8cddcbb8..f0689c144c 100644 --- a/crates/desktop/artcraft/src/threads/sora_task_polling_thread.rs +++ b/crates/desktop/artcraft/src/services/sora/threads/sora_task_polling_thread.rs @@ -1,11 +1,11 @@ -use crate::events::sendable_event_trait::SendableEvent; -use crate::events::sora::sora_image_generation_complete_event::SoraImageGenerationCompleteEvent; -use crate::events::sora::sora_image_generation_failed_event::SoraImageGenerationFailedEvent; -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::data_dir::trait_data_subdir::DataSubdir; -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::core::events::sendable_event_trait::SendableEvent; +use crate::services::sora::events::sora_image_generation_complete_event::SoraImageGenerationCompleteEvent; +use crate::services::sora::events::sora_image_generation_failed_event::SoraImageGenerationFailedEvent; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::core::state::data_dir::trait_data_subdir::DataSubdir; +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 errors::AnyhowResult; use log::{error, info}; use openai_sora_client::recipes::list_sora_task_status_with_session_auto_renew::{list_sora_task_status_with_session_auto_renew, StatusRequestArgs}; diff --git a/crates/desktop/artcraft/src/services/sora/windows/mod.rs b/crates/desktop/artcraft/src/services/sora/windows/mod.rs new file mode 100644 index 0000000000..3843c67df6 --- /dev/null +++ b/crates/desktop/artcraft/src/services/sora/windows/mod.rs @@ -0,0 +1 @@ +pub mod sora_login_window; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/windows/sora_login_window/clear_browsing_on_test_domain.rs b/crates/desktop/artcraft/src/services/sora/windows/sora_login_window/clear_browsing_on_test_domain.rs similarity index 100% rename from crates/desktop/artcraft/src/windows/sora_login_window/clear_browsing_on_test_domain.rs rename to crates/desktop/artcraft/src/services/sora/windows/sora_login_window/clear_browsing_on_test_domain.rs diff --git a/crates/desktop/artcraft/src/windows/sora_login_window/extract_sora_webview_cookies.rs b/crates/desktop/artcraft/src/services/sora/windows/sora_login_window/extract_sora_webview_cookies.rs similarity index 100% rename from crates/desktop/artcraft/src/windows/sora_login_window/extract_sora_webview_cookies.rs rename to crates/desktop/artcraft/src/services/sora/windows/sora_login_window/extract_sora_webview_cookies.rs diff --git a/crates/desktop/artcraft/src/windows/sora_login_window/keep_on_login_domains.rs b/crates/desktop/artcraft/src/services/sora/windows/sora_login_window/keep_on_login_domains.rs similarity index 87% rename from crates/desktop/artcraft/src/windows/sora_login_window/keep_on_login_domains.rs rename to crates/desktop/artcraft/src/services/sora/windows/sora_login_window/keep_on_login_domains.rs index 9c485ae030..6edb1de357 100644 --- a/crates/desktop/artcraft/src/windows/sora_login_window/keep_on_login_domains.rs +++ b/crates/desktop/artcraft/src/services/sora/windows/sora_login_window/keep_on_login_domains.rs @@ -1,4 +1,4 @@ -use crate::windows::sora_login_window::open_sora_login_window::SORA_LOGIN_URL; +use crate::services::sora::windows::sora_login_window::open_sora_login_window::SORA_LOGIN_URL; use anyhow::anyhow; use errors::AnyhowResult; use log::info; diff --git a/crates/desktop/artcraft/src/windows/sora_login_window/mod.rs b/crates/desktop/artcraft/src/services/sora/windows/sora_login_window/mod.rs similarity index 100% rename from crates/desktop/artcraft/src/windows/sora_login_window/mod.rs rename to crates/desktop/artcraft/src/services/sora/windows/sora_login_window/mod.rs diff --git a/crates/desktop/artcraft/src/windows/sora_login_window/open_sora_login_window.rs b/crates/desktop/artcraft/src/services/sora/windows/sora_login_window/open_sora_login_window.rs similarity index 88% rename from crates/desktop/artcraft/src/windows/sora_login_window/open_sora_login_window.rs rename to crates/desktop/artcraft/src/services/sora/windows/sora_login_window/open_sora_login_window.rs index d017bdd2a2..cb99a7a246 100644 --- a/crates/desktop/artcraft/src/windows/sora_login_window/open_sora_login_window.rs +++ b/crates/desktop/artcraft/src/services/sora/windows/sora_login_window/open_sora_login_window.rs @@ -1,7 +1,7 @@ -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::sora::sora_credential_manager::SoraCredentialManager; -use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies; -use crate::windows::sora_login_window::sora_login_thread::sora_login_thread; +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; diff --git a/crates/desktop/artcraft/src/windows/sora_login_window/sora_login_thread.rs b/crates/desktop/artcraft/src/services/sora/windows/sora_login_window/sora_login_thread.rs similarity index 86% rename from crates/desktop/artcraft/src/windows/sora_login_window/sora_login_thread.rs rename to crates/desktop/artcraft/src/services/sora/windows/sora_login_window/sora_login_thread.rs index 06cc0145a0..88daffb4a6 100644 --- a/crates/desktop/artcraft/src/windows/sora_login_window/sora_login_thread.rs +++ b/crates/desktop/artcraft/src/services/sora/windows/sora_login_window/sora_login_thread.rs @@ -1,11 +1,11 @@ -use crate::events::sendable_event_trait::SendableEvent; -use crate::events::sora::sora_login_success_event::SoraLoginSuccessEvent; -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::sora::sora_credential_holder::SoraCredentialHolder; -use crate::state::sora::sora_credential_manager::SoraCredentialManager; -use crate::utils::sora::initialize_sora_jwt_bearer_token::initialize_sora_jwt_bearer_token; -use crate::windows::sora_login_window::extract_sora_webview_cookies::extract_sora_webview_cookies; -use crate::windows::sora_login_window::open_sora_login_window::LOGIN_WINDOW_NAME; +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; diff --git a/crates/desktop/artcraft/src/services/storyteller/mod.rs b/crates/desktop/artcraft/src/services/storyteller/mod.rs new file mode 100644 index 0000000000..0d0b840411 --- /dev/null +++ b/crates/desktop/artcraft/src/services/storyteller/mod.rs @@ -0,0 +1 @@ +pub mod state; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/state/storyteller/mod.rs b/crates/desktop/artcraft/src/services/storyteller/state/mod.rs similarity index 100% rename from crates/desktop/artcraft/src/state/storyteller/mod.rs rename to crates/desktop/artcraft/src/services/storyteller/state/mod.rs diff --git a/crates/desktop/artcraft/src/state/storyteller/read_storyteller_credentials_from_disk.rs b/crates/desktop/artcraft/src/services/storyteller/state/read_storyteller_credentials_from_disk.rs similarity index 94% rename from crates/desktop/artcraft/src/state/storyteller/read_storyteller_credentials_from_disk.rs rename to crates/desktop/artcraft/src/services/storyteller/state/read_storyteller_credentials_from_disk.rs index a721ec05b9..4d214ce46c 100644 --- a/crates/desktop/artcraft/src/state/storyteller/read_storyteller_credentials_from_disk.rs +++ b/crates/desktop/artcraft/src/services/storyteller/state/read_storyteller_credentials_from_disk.rs @@ -1,4 +1,4 @@ -use crate::state::data_dir::app_data_root::AppDataRoot; +use crate::core::state::data_dir::app_data_root::AppDataRoot; use errors::AnyhowResult; use std::fs::read_to_string; use storyteller_client::credentials::storyteller_avt_cookie::StorytellerAvtCookie; diff --git a/crates/desktop/artcraft/src/state/storyteller/storyteller_credential_holder.rs b/crates/desktop/artcraft/src/services/storyteller/state/storyteller_credential_holder.rs similarity index 100% rename from crates/desktop/artcraft/src/state/storyteller/storyteller_credential_holder.rs rename to crates/desktop/artcraft/src/services/storyteller/state/storyteller_credential_holder.rs diff --git a/crates/desktop/artcraft/src/state/storyteller/storyteller_credential_manager.rs b/crates/desktop/artcraft/src/services/storyteller/state/storyteller_credential_manager.rs similarity index 86% rename from crates/desktop/artcraft/src/state/storyteller/storyteller_credential_manager.rs rename to crates/desktop/artcraft/src/services/storyteller/state/storyteller_credential_manager.rs index b9cdabf9ab..eb510e283a 100644 --- a/crates/desktop/artcraft/src/state/storyteller/storyteller_credential_manager.rs +++ b/crates/desktop/artcraft/src/services/storyteller/state/storyteller_credential_manager.rs @@ -1,8 +1,8 @@ -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 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 anyhow::anyhow; use errors::AnyhowResult; use log::{error, info, warn}; diff --git a/crates/desktop/artcraft/src/threads/mod.rs b/crates/desktop/artcraft/src/threads/mod.rs deleted file mode 100644 index eaae9fdf2e..0000000000 --- a/crates/desktop/artcraft/src/threads/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod discord_presence_thread; -pub mod main_window_thread; -pub mod sora_task_polling_thread; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/utils/sora/mod.rs b/crates/desktop/artcraft/src/utils/sora/mod.rs deleted file mode 100644 index 689972757f..0000000000 --- a/crates/desktop/artcraft/src/utils/sora/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod initialize_sora_jwt_bearer_token; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/windows/mod.rs b/crates/desktop/artcraft/src/windows/mod.rs deleted file mode 100644 index 564f15ab84..0000000000 --- a/crates/desktop/artcraft/src/windows/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod sora_login_window; -pub mod main_window; \ No newline at end of file diff --git a/crates/lib/api_clients/fal_client/Cargo.toml b/crates/lib/api_clients/fal_client/Cargo.toml index 91e806b917..15a2be53fa 100644 --- a/crates/lib/api_clients/fal_client/Cargo.toml +++ b/crates/lib/api_clients/fal_client/Cargo.toml @@ -22,8 +22,11 @@ errors = { workspace = true } # External anyhow = { workspace = true } +chrono = { workspace = true } fal = { version = "0.3.6", features = ["endpoints"] } futures = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } tokio = { workspace = true } url = { workspace = true } diff --git a/crates/lib/api_clients/fal_client/src/fal_error_plus.rs b/crates/lib/api_clients/fal_client/src/fal_error_plus.rs index 5168884a73..57259243b6 100644 --- a/crates/lib/api_clients/fal_client/src/fal_error_plus.rs +++ b/crates/lib/api_clients/fal_client/src/fal_error_plus.rs @@ -10,6 +10,8 @@ pub enum FalErrorPlus { AnyhowError(anyhow::Error), /// URL parse errors. UrlParseError(url::ParseError), + /// An endpoint we don't support yet. + UnhandledEndpoint(String), } impl Display for FalErrorPlus { @@ -18,6 +20,7 @@ impl Display for FalErrorPlus { Self::FalError(err) => write!(f, "FalErrorPlus::FalError: {:?}", err), Self::AnyhowError(err) => write!(f, "FalErrorPlus::AnyhowError: {:?}", err), Self::UrlParseError(err) => write!(f, "FalErrorPlus::UrlParseError: {:?}", err), + Self::UnhandledEndpoint(endpoint) => write!(f, "FalErrorPlus::UnhandledEndpoint: {:?}", endpoint), } } } diff --git a/crates/lib/api_clients/fal_client/src/lib.rs b/crates/lib/api_clients/fal_client/src/lib.rs index 82d82db2bd..2524e89e74 100644 --- a/crates/lib/api_clients/fal_client/src/lib.rs +++ b/crates/lib/api_clients/fal_client/src/lib.rs @@ -2,6 +2,13 @@ extern crate fal; +// Re-export +pub mod export { + pub use fal::*; +} + pub mod creds; pub mod fal_error_plus; +pub mod model; pub mod requests; +pub mod utils; diff --git a/crates/lib/api_clients/fal_client/src/model/enqueued_request.rs b/crates/lib/api_clients/fal_client/src/model/enqueued_request.rs new file mode 100644 index 0000000000..4fce8291ea --- /dev/null +++ b/crates/lib/api_clients/fal_client/src/model/enqueued_request.rs @@ -0,0 +1,41 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::model::fal_endpoint::FalEndpoint; +use crate::model::fal_request_id::FalRequestId; +use errors::AnyhowResult; +use fal::queue::{Queue, QueueResponse}; +use reqwest::Client; +use serde::de::DeserializeOwned; + +#[derive(Clone, Debug)] +pub struct EnqueuedRequest { + pub fal_endpoint: FalEndpoint, + pub request_id: FalRequestId, + pub response_url: String, + pub status_url: String, + pub cancel_url: String, +} + +impl EnqueuedRequest { + pub fn from_queue_response(queue: &Queue) -> AnyhowResult { + let endpoint = FalEndpoint::from_queue_response(queue); + Ok(Self { + fal_endpoint: endpoint.clone(), + request_id: FalRequestId(queue.payload.request_id.clone()), + response_url: queue.payload.response_url.clone(), + status_url: queue.payload.status_url.clone(), + cancel_url: queue.payload.cancel_url.clone(), + }) + } + + pub fn to_queue_response(&self, api_key: &FalApiKey) -> AnyhowResult> { + let client = Client::new(); + let endpoint = self.fal_endpoint.url(); + let payload = QueueResponse { + request_id: self.request_id.0.clone(), + response_url: self.response_url.clone(), + status_url: self.status_url.clone(), + cancel_url: self.cancel_url.clone(), + }; + Ok(Queue::new(client, endpoint, api_key.0.clone(), payload)) + } +} diff --git a/crates/lib/api_clients/fal_client/src/model/fal_endpoint.rs b/crates/lib/api_clients/fal_client/src/model/fal_endpoint.rs new file mode 100644 index 0000000000..75d866a243 --- /dev/null +++ b/crates/lib/api_clients/fal_client/src/model/fal_endpoint.rs @@ -0,0 +1,30 @@ +use fal::queue::Queue; +use serde::de::DeserializeOwned; + +const HUNYUAN_3D_2_BASE: &str = "fal-ai/hunyuan3d/v2"; + +const KLING16_IMAGE_TO_VIDEO: &str = "fal-ai/kling-video/v1.6/pro/image-to-video"; + +#[derive(Clone, Debug)] +pub enum FalEndpoint { + Kling16ImageToVideo, + Hunyuan3d2Base, + Other(String), +} + +impl FalEndpoint { + pub fn from_queue_response(queue: &Queue) -> Self { + match queue.endpoint.as_str() { + HUNYUAN_3D_2_BASE => FalEndpoint::Hunyuan3d2Base, + KLING16_IMAGE_TO_VIDEO => FalEndpoint::Kling16ImageToVideo, + _ => FalEndpoint::Other(queue.endpoint.clone()), + } + } + pub fn url(&self) -> &str { + match self { + Self::Hunyuan3d2Base => HUNYUAN_3D_2_BASE, + Self::Kling16ImageToVideo => KLING16_IMAGE_TO_VIDEO, + Self::Other(url) => url, + } + } +} diff --git a/crates/lib/api_clients/fal_client/src/model/fal_request_id.rs b/crates/lib/api_clients/fal_client/src/model/fal_request_id.rs new file mode 100644 index 0000000000..254cf8171f --- /dev/null +++ b/crates/lib/api_clients/fal_client/src/model/fal_request_id.rs @@ -0,0 +1,13 @@ +use std::fmt::Display; +use serde::{Deserialize, Serialize}; +use crate::model::fal_endpoint::FalEndpoint; + +/// A strongly typed request ID for the Fal API. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] +pub struct FalRequestId(pub String); + +impl Display for FalRequestId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, f) + } +} diff --git a/crates/lib/api_clients/fal_client/src/model/mod.rs b/crates/lib/api_clients/fal_client/src/model/mod.rs new file mode 100644 index 0000000000..07ae4fa566 --- /dev/null +++ b/crates/lib/api_clients/fal_client/src/model/mod.rs @@ -0,0 +1,3 @@ +pub mod enqueued_request; +pub mod fal_endpoint; +pub mod fal_request_id; \ No newline at end of file diff --git a/crates/lib/api_clients/fal_client/src/requests/enqueue_hunyuan2_image_to_3d.rs b/crates/lib/api_clients/fal_client/src/requests/enqueue_hunyuan2_image_to_3d.rs new file mode 100644 index 0000000000..546f8862f4 --- /dev/null +++ b/crates/lib/api_clients/fal_client/src/requests/enqueue_hunyuan2_image_to_3d.rs @@ -0,0 +1,69 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::fal_error_plus::FalErrorPlus; +use crate::model::enqueued_request::EnqueuedRequest; +use fal::endpoints::fal_ai::hunyuan3d::v2::v2; +use fal::endpoints::fal_ai::hunyuan3d::v2::Hunyuan3DInput; +use fal_client::file_to_base64_url::file_to_base64_url; +use futures::StreamExt; +use std::io::Write; +use std::path::Path; + +pub struct Hunyuan2Args<'a, P: AsRef> { + pub image_path: P, + pub api_key: &'a FalApiKey, +} + +pub async fn enqueue_hunyuan2_image_to_3d>(args: Hunyuan2Args<'_, P>) -> Result { + let image_url = file_to_base64_url(args.image_path)?; + + /* + TODO: Handle error messages - + FalError(FalError(Other("{\"detail\": \"Invalid Key Authorization header format. Expected ':'.\"}"))) + FalError(FalError(Other("{\"detail\": \"No user found for Key ID and Secret\"}"))) + */ + + let request = Hunyuan3DInput { + input_image_url: image_url, + textured_mesh: Some(true), + guidance_scale: None, + num_inference_steps: None, + octree_resolution: None, + seed: None, + }; + + let result = v2(request) + .with_api_key(&args.api_key.0) + .queue() + .await?; + + Ok(EnqueuedRequest::from_queue_response(&result)?) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use testing::test_file_path::test_file_path; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + let image = test_file_path("test_data/image/juno.jpg")?; + + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = Hunyuan2Args { + image_path: image, + api_key: &api_key, + }; + + let result = enqueue_hunyuan2_image_to_3d(args).await?; + + Ok(()) + } +} diff --git a/crates/lib/api_clients/fal_client/src/requests/enqueue_kling_16_image_to_video.rs b/crates/lib/api_clients/fal_client/src/requests/enqueue_kling_16_image_to_video.rs new file mode 100644 index 0000000000..73282628bf --- /dev/null +++ b/crates/lib/api_clients/fal_client/src/requests/enqueue_kling_16_image_to_video.rs @@ -0,0 +1,85 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::fal_error_plus::FalErrorPlus; +use crate::model::enqueued_request::EnqueuedRequest; +use fal::endpoints::fal_ai::kling_video::v1_6::pro::image_to_video::{image_to_video, ProImageToVideoRequest}; +use fal_client::file_to_base64_url::file_to_base64_url; +use futures::StreamExt; +use std::io::Write; +use std::path::Path; +use url::Url; + +pub struct Kling16Args<'a, P: AsRef> { + pub image_path: P, + pub api_key: &'a FalApiKey, + pub duration: Kling16Duration, +} + +#[derive(Copy, Clone, Debug)] +pub enum Kling16Duration { + Default, + FiveSeconds, + TenSeconds, +} + +pub async fn enqueue_kling_16_image_to_video>(args: Kling16Args<'_, P>) -> Result { + let image_url = file_to_base64_url(args.image_path)?; + + /* + TODO: Handle error messages - + FalError(FalError(Other("{\"detail\": \"Invalid Key Authorization header format. Expected ':'.\"}"))) + FalError(FalError(Other("{\"detail\": \"No user found for Key ID and Secret\"}"))) + */ + + let duration = match args.duration{ + Kling16Duration::Default => None, + Kling16Duration::FiveSeconds => Some("5".to_string()), // Gross... + Kling16Duration::TenSeconds => Some("10".to_string()), + }; + + let request = ProImageToVideoRequest { + image_url, + prompt: "".to_string(), + aspect_ratio: None, + cfg_scale: None, + duration, + negative_prompt: None, + tail_image_url: None, + }; + + let result = image_to_video(request) + .with_api_key(&args.api_key.0) + .queue() + .await?; + + Ok(EnqueuedRequest::from_queue_response(&result)?) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::enqueue_kling_16_image_to_video::{enqueue_kling_16_image_to_video, Kling16Args, Kling16Duration}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use testing::test_file_path::test_file_path; + + #[tokio::test] + #[ignore] + async fn test_kling16_video() -> AnyhowResult<()> { + let image = test_file_path("test_data/image/juno.jpg")?; + + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = Kling16Args { + image_path: image, + api_key: &api_key, + duration: Kling16Duration::Default, + }; + + let result = enqueue_kling_16_image_to_video(args).await?; + + Ok(()) + } +} diff --git a/crates/lib/api_clients/fal_client/src/requests/mod.rs b/crates/lib/api_clients/fal_client/src/requests/mod.rs index 6a40215b31..3544169e94 100644 --- a/crates/lib/api_clients/fal_client/src/requests/mod.rs +++ b/crates/lib/api_clients/fal_client/src/requests/mod.rs @@ -1 +1,3 @@ +pub mod enqueue_hunyuan2_image_to_3d; +pub mod enqueue_kling_16_image_to_video; pub mod remove_background_rembg; \ No newline at end of file diff --git a/crates/lib/api_clients/fal_client/src/utils/clone_queue_payload.rs b/crates/lib/api_clients/fal_client/src/utils/clone_queue_payload.rs new file mode 100644 index 0000000000..b8b78bee1a --- /dev/null +++ b/crates/lib/api_clients/fal_client/src/utils/clone_queue_payload.rs @@ -0,0 +1,7 @@ +use crate::utils::clone_queue_response::clone_queue_response; +use fal::queue::{Queue, QueueResponse}; +use serde::de::DeserializeOwned; + +pub fn clone_queue_payload(queue: &Queue) -> QueueResponse { + clone_queue_response(&queue.payload) +} diff --git a/crates/lib/api_clients/fal_client/src/utils/clone_queue_response.rs b/crates/lib/api_clients/fal_client/src/utils/clone_queue_response.rs new file mode 100644 index 0000000000..8caee7302e --- /dev/null +++ b/crates/lib/api_clients/fal_client/src/utils/clone_queue_response.rs @@ -0,0 +1,10 @@ +use fal::queue::QueueResponse; + +pub fn clone_queue_response(response: &QueueResponse) -> QueueResponse { + QueueResponse { + request_id: response.request_id.clone(), + response_url: response.response_url.clone(), + status_url: response.status_url.clone(), + cancel_url: response.cancel_url.clone(), + } +} diff --git a/crates/lib/api_clients/fal_client/src/utils/image_file_to_base64_url.rs b/crates/lib/api_clients/fal_client/src/utils/image_file_to_base64_url.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/lib/api_clients/fal_client/src/utils/mod.rs b/crates/lib/api_clients/fal_client/src/utils/mod.rs new file mode 100644 index 0000000000..31c3f2080e --- /dev/null +++ b/crates/lib/api_clients/fal_client/src/utils/mod.rs @@ -0,0 +1,3 @@ +pub mod clone_queue_payload; +pub mod clone_queue_response; +pub mod queue_status_checker; \ No newline at end of file diff --git a/crates/lib/api_clients/fal_client/src/utils/queue_status_checker.rs b/crates/lib/api_clients/fal_client/src/utils/queue_status_checker.rs new file mode 100644 index 0000000000..e93e300cf3 --- /dev/null +++ b/crates/lib/api_clients/fal_client/src/utils/queue_status_checker.rs @@ -0,0 +1,70 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::fal_error_plus::FalErrorPlus; +use crate::model::enqueued_request::EnqueuedRequest; +use crate::model::fal_endpoint::FalEndpoint; +use fal::endpoints::fal_ai::hunyuan3d::v2::ObjectOutput; +use fal::endpoints::fal_ai::kling_video::v1_6::pro::effects::I2VOutput; +use fal::prelude::QueueResponse; +use fal::queue::{Queue, QueueStatus}; +use reqwest::Client; +use serde::de::DeserializeOwned; + +pub struct QueueStatusChecker { + api_key: FalApiKey, +} + +impl QueueStatusChecker { + pub fn new(api_key: FalApiKey) -> Self { + Self { api_key } + } + + pub async fn check_status(&self, request: &EnqueuedRequest) -> Result { + match &request.fal_endpoint { + FalEndpoint::Hunyuan3d2Base => { + let queue = self.make_queue::(&request); + let status = queue.status(false).await?; + Ok(status) + } + FalEndpoint::Kling16ImageToVideo => { + let queue = self.make_queue::(&request); + let status = queue.status(false).await?; + Ok(status) + } + FalEndpoint::Other(endpoint) => { + Err(FalErrorPlus::UnhandledEndpoint(format!("Unsupported endpoint: {}", endpoint))) + } + } + } + + pub async fn get_download_url(&self, request: &EnqueuedRequest) -> Result { + match &request.fal_endpoint { + FalEndpoint::Hunyuan3d2Base => { + let queue = self.make_queue::(&request); + let status = queue.response().await?; + Ok(status.model_mesh.url) + } + FalEndpoint::Kling16ImageToVideo => { + let queue = self.make_queue::(&request); + let status = queue.response().await?; + Ok(status.video.url) + } + FalEndpoint::Other(endpoint) => { + Err(FalErrorPlus::UnhandledEndpoint(format!("Unsupported endpoint: {}", endpoint))) + } + } + } + + pub fn make_queue(&self, request: &EnqueuedRequest) -> Queue { + let client = Client::new(); + let endpoint = request.fal_endpoint.url(); + + let payload = QueueResponse { + request_id: request.request_id.0.clone(), + response_url: request.response_url.clone(), + status_url: request.status_url.clone(), + cancel_url: request.cancel_url.clone(), + }; + + Queue::new(client, endpoint, self.api_key.0.clone(), payload) + } +} \ No newline at end of file diff --git a/crates/lib/api_clients/storyteller_client/Cargo.toml b/crates/lib/api_clients/storyteller_client/Cargo.toml index 55dd190279..03d6332dac 100644 --- a/crates/lib/api_clients/storyteller_client/Cargo.toml +++ b/crates/lib/api_clients/storyteller_client/Cargo.toml @@ -22,6 +22,7 @@ enums_public = { workspace = true } errors = { workspace = true } idempotency = { workspace = true } tokens = { workspace = true } +mimetypes = { workspace = true } # External anyhow = { workspace = true } diff --git a/crates/lib/api_clients/storyteller_client/src/api_error.rs b/crates/lib/api_clients/storyteller_client/src/error/api_error.rs similarity index 100% rename from crates/lib/api_clients/storyteller_client/src/api_error.rs rename to crates/lib/api_clients/storyteller_client/src/error/api_error.rs diff --git a/crates/lib/api_clients/storyteller_client/src/error/client_error.rs b/crates/lib/api_clients/storyteller_client/src/error/client_error.rs new file mode 100644 index 0000000000..1bf04115c1 --- /dev/null +++ b/crates/lib/api_clients/storyteller_client/src/error/client_error.rs @@ -0,0 +1,35 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::io; +use std::path::PathBuf; + +/// An error arising from the client, either before or after the request is sent. +#[derive(Debug)] +pub enum ClientError { + /// Could not determine the filetype for the file. + FileTypeNotKnown(PathBuf), + + /// Could not handle the file type. + FileTypeNotHandled(PathBuf), + + /// An error doing file I/O (on our side) + IoError(io::Error), +} + +impl Error for ClientError {} + +impl Display for ClientError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::FileTypeNotKnown(path) => write!(f, "Could not determine the filetype for the file: {:?}", path), + Self::FileTypeNotHandled(path) => write!(f, "Could not handle the file type for the file: {:?}", path), + Self::IoError(err) => write!(f, "IO error: {}", err), + } + } +} + +impl From for ClientError { + fn from(error: io::Error) -> Self { + ClientError::IoError(error) + } +} diff --git a/crates/lib/api_clients/storyteller_client/src/error/mod.rs b/crates/lib/api_clients/storyteller_client/src/error/mod.rs new file mode 100644 index 0000000000..4b238e7786 --- /dev/null +++ b/crates/lib/api_clients/storyteller_client/src/error/mod.rs @@ -0,0 +1,3 @@ +pub mod api_error; +pub mod client_error; +pub mod storyteller_error; \ No newline at end of file diff --git a/crates/lib/api_clients/storyteller_client/src/error/storyteller_error.rs b/crates/lib/api_clients/storyteller_client/src/error/storyteller_error.rs new file mode 100644 index 0000000000..1c0fdec7e8 --- /dev/null +++ b/crates/lib/api_clients/storyteller_client/src/error/storyteller_error.rs @@ -0,0 +1,32 @@ +use crate::error::api_error::ApiError; +use crate::error::client_error::ClientError; +use std::error::Error; + +#[derive(Debug)] +pub enum StorytellerError { + Api(ApiError), + Client(ClientError), +} + +impl Error for StorytellerError {} + +impl std::fmt::Display for StorytellerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StorytellerError::Api(e) => write!(f, "API error: {:?}", e), + StorytellerError::Client(e) => write!(f, "Client error: {:?}", e), + } + } +} + +impl From for StorytellerError { + fn from(error: ApiError) -> Self { + StorytellerError::Api(error) + } +} + +impl From for StorytellerError { + fn from(error: ClientError) -> Self { + StorytellerError::Client(error) + } +} diff --git a/crates/lib/api_clients/storyteller_client/src/lib.rs b/crates/lib/api_clients/storyteller_client/src/lib.rs index d0b6be4a4e..e1d6792184 100644 --- a/crates/lib/api_clients/storyteller_client/src/lib.rs +++ b/crates/lib/api_clients/storyteller_client/src/lib.rs @@ -1,7 +1,8 @@ //! An API client for Storyteller / FakeYou -pub mod api_error; pub mod credentials; +pub mod error; pub mod media_files; +pub mod recipes; pub mod shared_response_types; pub mod utils; diff --git a/crates/lib/api_clients/storyteller_client/src/media_files/get_media_file.rs b/crates/lib/api_clients/storyteller_client/src/media_files/get_media_file.rs index f83104fca4..78d897cf86 100644 --- a/crates/lib/api_clients/storyteller_client/src/media_files/get_media_file.rs +++ b/crates/lib/api_clients/storyteller_client/src/media_files/get_media_file.rs @@ -1,4 +1,4 @@ -use crate::api_error::ApiError; +use crate::error::api_error::ApiError; use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails; use crate::shared_response_types::media_links::MediaLinks; use crate::shared_response_types::simple_entity_stats::SimpleEntityStats; diff --git a/crates/lib/api_clients/storyteller_client/src/media_files/mod.rs b/crates/lib/api_clients/storyteller_client/src/media_files/mod.rs index c9b77448b3..f37ac72cb9 100644 --- a/crates/lib/api_clients/storyteller_client/src/media_files/mod.rs +++ b/crates/lib/api_clients/storyteller_client/src/media_files/mod.rs @@ -1,2 +1,4 @@ pub mod get_media_file; pub mod upload_image_media_file_from_file; +pub mod upload_new_engine_asset_from_file; +pub mod upload_video_media_file_from_file; diff --git a/crates/lib/api_clients/storyteller_client/src/media_files/upload_image_media_file_from_file.rs b/crates/lib/api_clients/storyteller_client/src/media_files/upload_image_media_file_from_file.rs index eb9faa09de..6b9f1083f7 100644 --- a/crates/lib/api_clients/storyteller_client/src/media_files/upload_image_media_file_from_file.rs +++ b/crates/lib/api_clients/storyteller_client/src/media_files/upload_image_media_file_from_file.rs @@ -1,5 +1,5 @@ -use crate::api_error::ApiError; use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; +use crate::error::api_error::ApiError; use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails; use crate::shared_response_types::media_links::MediaLinks; use crate::shared_response_types::simple_entity_stats::SimpleEntityStats; diff --git a/crates/lib/api_clients/storyteller_client/src/media_files/upload_new_engine_asset_from_file.rs b/crates/lib/api_clients/storyteller_client/src/media_files/upload_new_engine_asset_from_file.rs new file mode 100644 index 0000000000..054f283b55 --- /dev/null +++ b/crates/lib/api_clients/storyteller_client/src/media_files/upload_new_engine_asset_from_file.rs @@ -0,0 +1,93 @@ +use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; +use crate::error::api_error::ApiError; +use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails; +use crate::shared_response_types::media_links::MediaLinks; +use crate::shared_response_types::simple_entity_stats::SimpleEntityStats; +use crate::shared_response_types::user_details_light::UserDetailsLight; +use crate::utils::api_host::ApiHost; +use crate::utils::constants::{APPLICATION_JSON, USER_AGENT}; +use crate::utils::filter_bad_response::filter_bad_response; +use crate::utils::http_get_anonymous::http_get_anonymous; +use chrono::{DateTime, Utc}; +use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType; +use enums::by_table::media_files::media_file_class::MediaFileClass; +use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory; +use enums::by_table::media_files::media_file_type::MediaFileType; +use enums::by_table::model_weights::weights_category::WeightsCategory; +use enums::common::visibility::Visibility; +use enums::no_table::style_transfer::style_transfer_name::StyleTransferName; +use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType; +use idempotency::uuid::generate_random_uuid; +use log::debug; +use reqwest::multipart::Form; +use reqwest::Client; +use serde_derive::{Deserialize, Serialize}; +use std::fs::File; +use std::io::Read; +use std::path::Path; +use tokens::tokens::batch_generations::BatchGenerationToken; +use tokens::tokens::media_files::MediaFileToken; +use tokens::tokens::model_weights::ModelWeightToken; +use tokens::tokens::prompts::PromptToken; +use uuid::uuid; + +// TODO: The naming of these methods is ridiculous. It needs to be cleaned up. + +/// Upload an image media file from a file. +pub async fn upload_new_engine_asset_from_file>( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + path: P, +) -> Result { + + let url = get_route(api_host); + + debug!("Requesting {:?}", &url); + + //let mut file = File::open(path)?; + + let client = Client::builder() + .gzip(true) + .build()?; + + let form = Form::new() + .text("uuid_idempotency_token", generate_random_uuid()) + .text("engine_category", "object") // TODO: Which type should we use? + .file("file", path) + .await?; + + let mut request_builder = client.post(url) + .header("User-Agent", USER_AGENT) + .header("Accept", APPLICATION_JSON); + + if let Some(creds) = maybe_creds { + if let Some(header) = &creds.maybe_as_cookie_header() { + request_builder = request_builder.header("Cookie", header); + } + } + + let response = request_builder + .multipart(form) + .send() + .await?; + + let response = filter_bad_response(response).await?; + let response_body = &response.text().await?; + + let media_file = serde_json::from_str(&response_body)?; + + Ok(media_file) +} + +fn get_route(api_host: &ApiHost) -> String { + let api_hostname = api_host.to_api_hostname(); + format!("https://{}/v1/media_files/upload/new_engine_asset", api_hostname) +} + +// TODO(bt,2025-04-22): Share API definitions between client and server in common crate. + +#[derive(Deserialize, Debug)] +pub struct UploadImageMediaFileSuccessResponse { + pub success: bool, + pub media_file_token: MediaFileToken, +} diff --git a/crates/lib/api_clients/storyteller_client/src/media_files/upload_video_media_file_from_file.rs b/crates/lib/api_clients/storyteller_client/src/media_files/upload_video_media_file_from_file.rs new file mode 100644 index 0000000000..aecc6805df --- /dev/null +++ b/crates/lib/api_clients/storyteller_client/src/media_files/upload_video_media_file_from_file.rs @@ -0,0 +1,91 @@ +use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; +use crate::error::api_error::ApiError; +use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails; +use crate::shared_response_types::media_links::MediaLinks; +use crate::shared_response_types::simple_entity_stats::SimpleEntityStats; +use crate::shared_response_types::user_details_light::UserDetailsLight; +use crate::utils::api_host::ApiHost; +use crate::utils::constants::{APPLICATION_JSON, USER_AGENT}; +use crate::utils::filter_bad_response::filter_bad_response; +use crate::utils::http_get_anonymous::http_get_anonymous; +use chrono::{DateTime, Utc}; +use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType; +use enums::by_table::media_files::media_file_class::MediaFileClass; +use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory; +use enums::by_table::media_files::media_file_type::MediaFileType; +use enums::by_table::model_weights::weights_category::WeightsCategory; +use enums::common::visibility::Visibility; +use enums::no_table::style_transfer::style_transfer_name::StyleTransferName; +use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType; +use idempotency::uuid::generate_random_uuid; +use log::debug; +use reqwest::multipart::Form; +use reqwest::Client; +use serde_derive::{Deserialize, Serialize}; +use std::fs::File; +use std::io::Read; +use std::path::Path; +use tokens::tokens::batch_generations::BatchGenerationToken; +use tokens::tokens::media_files::MediaFileToken; +use tokens::tokens::model_weights::ModelWeightToken; +use tokens::tokens::prompts::PromptToken; +use uuid::uuid; + +// TODO(bt,2025-04-22): Share API definitions between client and server in common crate. + +#[derive(Deserialize, Debug)] +pub struct UploadVideoMediaFileSuccessResponse { + pub success: bool, + pub media_file_token: MediaFileToken, +} + +/// Upload a video media file from a file. +pub async fn upload_video_media_file_from_file>( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + path: P, +) -> Result { + + let url = get_route(api_host); + + debug!("Requesting {:?}", &url); + + //let mut file = File::open(path)?; + + let client = Client::builder() + .gzip(true) + .build()?; + + let form = Form::new() + .text("uuid_idempotency_token", generate_random_uuid()) + .file("file", path) + .await?; + + let mut request_builder = client.post(url) + .header("User-Agent", USER_AGENT) + .header("Accept", APPLICATION_JSON); + + if let Some(creds) = maybe_creds { + if let Some(header) = &creds.maybe_as_cookie_header() { + request_builder = request_builder.header("Cookie", header); + } + } + + let response = request_builder + .multipart(form) + .send() + .await?; + + let response = filter_bad_response(response).await?; + let response_body = &response.text().await?; + + let media_file = serde_json::from_str(&response_body)?; + + Ok(media_file) +} + +fn get_route(api_host: &ApiHost) -> String { + let api_hostname = api_host.to_api_hostname(); + format!("https://{}/v1/media_files/upload/new_video", api_hostname) +} + diff --git a/crates/lib/api_clients/storyteller_client/src/recipes/mod.rs b/crates/lib/api_clients/storyteller_client/src/recipes/mod.rs new file mode 100644 index 0000000000..1232fbe14e --- /dev/null +++ b/crates/lib/api_clients/storyteller_client/src/recipes/mod.rs @@ -0,0 +1 @@ +pub mod upload_media_file_from_file; \ No newline at end of file diff --git a/crates/lib/api_clients/storyteller_client/src/recipes/upload_media_file_from_file.rs b/crates/lib/api_clients/storyteller_client/src/recipes/upload_media_file_from_file.rs new file mode 100644 index 0000000000..714da66e23 --- /dev/null +++ b/crates/lib/api_clients/storyteller_client/src/recipes/upload_media_file_from_file.rs @@ -0,0 +1,63 @@ +use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; +use crate::error::client_error::ClientError; +use crate::error::storyteller_error::StorytellerError; +use crate::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file; +use crate::media_files::upload_new_engine_asset_from_file::upload_new_engine_asset_from_file; +use crate::media_files::upload_video_media_file_from_file::upload_video_media_file_from_file; +use crate::utils::api_host::ApiHost; +use mimetypes::mimetype_info::file_extension::FileExtension; +use mimetypes::mimetype_info::mimetype_info::MimetypeInfo; +use serde_derive::Deserialize; +use std::path::Path; +use tokens::tokens::media_files::MediaFileToken; + +#[derive(Deserialize, Debug)] +pub struct UploadMediaFileSuccessResponse { + pub success: bool, + pub media_file_token: MediaFileToken, +} + +/// Upload an image media file from a file. +pub async fn upload_media_file_from_file>( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + path: P, +) -> Result { + + let maybe_type = MimetypeInfo::get_for_path(path.as_ref()) + .map_err(|err| ClientError::from(err))? + .map(|mime| mime.file_extension()) + .flatten(); + + match maybe_type { + Some(FileExtension::Glb) => { + match upload_new_engine_asset_from_file(api_host, maybe_creds, path).await { + Ok(result) => Ok(UploadMediaFileSuccessResponse { + success: result.success, + media_file_token: result.media_file_token, + }), + Err(err) => Err(StorytellerError::Api(err)), + } + } + Some(FileExtension::Mp4) => { + match upload_video_media_file_from_file(api_host, maybe_creds, path).await { + Ok(result) => Ok(UploadMediaFileSuccessResponse { + success: result.success, + media_file_token: result.media_file_token, + }), + Err(err) => Err(StorytellerError::Api(err)), + } + } + Some(FileExtension::Png) | Some(FileExtension::Jpg) | Some(FileExtension::Gif) | Some(FileExtension::Webp) => { + match upload_image_media_file_from_file(api_host, maybe_creds, path).await { + Ok(result) => Ok(UploadMediaFileSuccessResponse { + success: result.success, + media_file_token: result.media_file_token, + }), + Err(err) => Err(StorytellerError::Api(err)), + } + } + Some(_) => Err(StorytellerError::Client(ClientError::FileTypeNotHandled(path.as_ref().to_path_buf()))), + None => Err(StorytellerError::Client(ClientError::FileTypeNotKnown(path.as_ref().to_path_buf()))), + } +} diff --git a/crates/lib/api_clients/storyteller_client/src/utils/filter_bad_response.rs b/crates/lib/api_clients/storyteller_client/src/utils/filter_bad_response.rs index 3bd27d1b17..aecf663d3c 100644 --- a/crates/lib/api_clients/storyteller_client/src/utils/filter_bad_response.rs +++ b/crates/lib/api_clients/storyteller_client/src/utils/filter_bad_response.rs @@ -1,7 +1,6 @@ -use anyhow::anyhow; -use crate::api_error::ApiError; +use crate::error::api_error::ApiError; use crate::utils::status_codes::*; -use reqwest::StatusCode; +use anyhow::anyhow; /// Pass request errors to this method for standard error handling behavior. /// This function needs to take temporary ownership, but will return it to the caller. diff --git a/crates/lib/api_clients/storyteller_client/src/utils/http_get_anonymous.rs b/crates/lib/api_clients/storyteller_client/src/utils/http_get_anonymous.rs index ece6bcb577..f4c66d20c7 100644 --- a/crates/lib/api_clients/storyteller_client/src/utils/http_get_anonymous.rs +++ b/crates/lib/api_clients/storyteller_client/src/utils/http_get_anonymous.rs @@ -1,5 +1,5 @@ +use crate::error::api_error::ApiError; use reqwest::Client; -use crate::api_error::ApiError; const USER_AGENT: &str = "storyteller-client/1.0"; pub async fn http_get_anonymous(url: String) -> Result { diff --git a/crates/lib/files/file_to_base64_url/src/file_to_base64_url.rs b/crates/lib/files/file_to_base64_url/src/file_to_base64_url.rs index 6e6097b8be..49040ff512 100644 --- a/crates/lib/files/file_to_base64_url/src/file_to_base64_url.rs +++ b/crates/lib/files/file_to_base64_url/src/file_to_base64_url.rs @@ -1,5 +1,5 @@ use anyhow::anyhow; -use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE, BASE64_URL_SAFE_NO_PAD}; +use base64::prelude::BASE64_STANDARD; use base64::Engine; use errors::AnyhowResult; use mimetypes::mimetype_info::mimetype_info::MimetypeInfo; @@ -24,3 +24,22 @@ pub fn file_to_base64_url>(path: P) -> AnyhowResult { _ => Err(anyhow!("unknown or unsupported mime type: {}", mime.mime_type())), } } + +#[cfg(test)] +mod tests { + use errors::AnyhowResult; + use testing::test_file_path::test_file_path; + use crate::file_to_base64_url::file_to_base64_url; + + #[test] + fn test_file_to_base64_url() -> AnyhowResult<()> { + let path = test_file_path("test_data/image/juno.jpg")?; + let base64_url = file_to_base64_url(path)?; + + assert!(base64_url.starts_with("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEBLAEsAAD")); + assert!(base64_url.ends_with("z8c2LAteJ5zOIDcX5m04K3P/2Q==")); + assert_eq!(base64_url.len(), 223991); + + Ok(()) + } +} \ No newline at end of file diff --git a/crates/lib/files/mimetypes/src/custom/custom_infer.rs b/crates/lib/files/mimetypes/src/custom/custom_infer.rs new file mode 100644 index 0000000000..9a27dd7af9 --- /dev/null +++ b/crates/lib/files/mimetypes/src/custom/custom_infer.rs @@ -0,0 +1,32 @@ +use infer::Infer; +use std::sync::LazyLock; + +pub (crate) static CUSTOM_INFER: LazyLock = LazyLock::new(|| { + let mut infer = Infer::new(); + add_custom_types(&mut infer); + infer +}); + +fn add_custom_types(infer: &mut Infer) { + infer.add("model/gltf-binary", "glb", gltf_matcher); +} + +fn gltf_matcher(buf: &[u8]) -> bool { + // Magic is 'glTF' (103 108 84 70) + // https://github.com/KhronosGroup/glTF/blob/main/extensions/1.0/Khronos/KHR_binary_glTF/README.md + buf.len() >= 4 && buf[0] == 103 && buf[1] == 108 && buf[2] == 84 && buf[3] == 70 +} + +#[cfg(test)] +mod tests { + use crate::custom::custom_infer::CUSTOM_INFER; + use testing::test_file_path::test_file_path; + + #[test] + fn test_gltf() -> anyhow::Result<()> { + let path = test_file_path("test_data/3d/hanashi.glb")?; + let maybe_type = CUSTOM_INFER.get_from_path(path)?.unwrap(); + assert_eq!(maybe_type.mime_type(), "model/gltf-binary"); + Ok(()) + } +} diff --git a/crates/lib/files/mimetypes/src/custom/mod.rs b/crates/lib/files/mimetypes/src/custom/mod.rs new file mode 100644 index 0000000000..69519270dd --- /dev/null +++ b/crates/lib/files/mimetypes/src/custom/mod.rs @@ -0,0 +1 @@ +pub (crate) mod custom_infer; \ No newline at end of file diff --git a/crates/lib/files/mimetypes/src/lib.rs b/crates/lib/files/mimetypes/src/lib.rs index 4d04fd6188..286b1abf88 100644 --- a/crates/lib/files/mimetypes/src/lib.rs +++ b/crates/lib/files/mimetypes/src/lib.rs @@ -19,6 +19,7 @@ #![allow(dead_code)] #![allow(non_snake_case)] +pub (crate) mod custom; pub mod mimetype_for_bytes; pub mod mimetype_for_file; pub mod mimetype_info; diff --git a/crates/lib/files/mimetypes/src/mimetype_info/file_extension.rs b/crates/lib/files/mimetypes/src/mimetype_info/file_extension.rs index 4b88194ef5..aa00627ac2 100644 --- a/crates/lib/files/mimetypes/src/mimetype_info/file_extension.rs +++ b/crates/lib/files/mimetypes/src/mimetype_info/file_extension.rs @@ -8,6 +8,7 @@ pub enum FileExtension { Aac, Flac, Gif, + Glb, Jpg, Mov, Mp3, @@ -26,6 +27,7 @@ impl FileExtension { "audio/aac" => Some(FileExtension::Aac), "audio/flac" => Some(FileExtension::Flac), "image/gif" => Some(FileExtension::Gif), + "model/gltf-binary" => Some(FileExtension::Glb), "image/jpeg" => Some(FileExtension::Jpg), "video/quicktime" => Some(FileExtension::Mov), "audio/mpeg" => Some(FileExtension::Mp3), @@ -53,6 +55,7 @@ impl FileExtension { FileExtension::Aac => (".aac", "aac"), FileExtension::Flac => (".flac", "flac"), FileExtension::Gif => (".gif", "gif"), + FileExtension::Glb => (".glb", "glb"), FileExtension::Jpg => (".jpg", "jpg"), FileExtension::Mov => (".mov", "mov"), FileExtension::Mp3 => (".mp3", "mp3"), diff --git a/crates/lib/files/mimetypes/src/mimetype_info/mimetype_info.rs b/crates/lib/files/mimetypes/src/mimetype_info/mimetype_info.rs index c8336bc908..46314838e3 100644 --- a/crates/lib/files/mimetypes/src/mimetype_info/mimetype_info.rs +++ b/crates/lib/files/mimetypes/src/mimetype_info/mimetype_info.rs @@ -1,3 +1,4 @@ +use crate::custom::custom_infer::CUSTOM_INFER; use crate::mimetype_info::file_extension::FileExtension; use infer::Type; use std::io; @@ -14,12 +15,12 @@ pub struct MimetypeInfo { impl MimetypeInfo { pub fn get_for_bytes(bytes: &[u8]) -> Option { - infer::get(bytes) + CUSTOM_INFER.get(bytes) .map(|t| Self::for_type(t)) } pub fn get_for_path>(path: P) -> io::Result> { - infer::get_from_path(path) + CUSTOM_INFER.get_from_path(path) .map(|maybe_type| maybe_type.map(|t| Self::for_type(t))) } @@ -62,8 +63,8 @@ mod tests { } mod for_files { - use testing::test_file_path::test_file_path; use super::*; + use testing::test_file_path::test_file_path; #[test] fn mp3() -> anyhow::Result<()> { @@ -76,5 +77,17 @@ mod tests { assert_eq!(ext.extension_without_period(), "mp3"); Ok(()) } + + #[test] + fn glb() -> anyhow::Result<()> { + let path = test_file_path("test_data/3d/hanashi.glb")?; + let t = MimetypeInfo::get_for_path(path)?.expect("should detect type"); + assert_eq!(t.mime_type(), "model/gltf-binary"); + let ext = t.file_extension.expect("should have extension"); + assert_eq!(ext, FileExtension::Glb); + assert_eq!(ext.extension_with_period(), ".glb"); + assert_eq!(ext.extension_without_period(), "glb"); + Ok(()) + } } } diff --git a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx index 1a4a182308..4aa1bbb0bb 100644 --- a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx +++ b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx @@ -4,6 +4,7 @@ import { Transition, TransitionChild } from "@headlessui/react"; import { createPortal } from "react-dom"; import dayjs from "dayjs"; import { faDownToLine } from "@fortawesome/pro-solid-svg-icons"; +import { FalKlingImageToVideo, FalHunyuanImageTo3d } from "@storyteller/tauri-api" interface LightboxModalProps { isOpen: boolean; @@ -116,6 +117,37 @@ export function LightboxModal({ {/* buttons with spacing */} {(onAddToSceneClicked && downloadUrl) || downloadUrl ? (
+ + + + + {onAddToSceneClicked && downloadUrl && (