From 691f27eb2387564749fd8e9fc238fc4bc4bde8a4 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 19 Jun 2025 20:41:46 -0400 Subject: [PATCH] misc: refactor client, move endpoints, more models, local testing, etc. etc. (#683) --- .../artcraft/capabilities/default.json | 3 + .../image/enqueue_text_to_image_command.rs | 10 +- .../enqueue/image/handle_image_artcraft.rs | 10 +- .../video/enqueue_image_to_video_command.rs | 21 +-- .../enqueue/video/handle_video_artcraft.rs | 26 ++-- .../enqueue/video/handle_video_fal.rs | 8 +- crates/desktop/artcraft/src/core/mod.rs | 1 + .../artcraft/src/core/model/image_models.rs | 0 crates/desktop/artcraft/src/core/model/mod.rs | 2 + .../artcraft/src/core/model/video_models.rs | 22 ++++ .../state/app_env_configs/app_env_configs.rs | 38 ++++++ .../app_env_configs_serializeable.rs | 34 +++++ .../src/core/state/app_env_configs/mod.rs | 2 + .../core/state/data_dir/app_settings_dir.rs | 6 +- crates/desktop/artcraft/src/core/state/mod.rs | 1 + crates/desktop/artcraft/src/lib.rs | 5 + .../generate_flux_1_dev_text_to_image.rs | 65 ++------- .../generate_flux_1_schnell_text_to_image.rs | 65 ++------- .../generate_flux_pro_11_text_to_image.rs | 65 ++------- ...enerate_flux_pro_11_ultra_text_to_image.rs | 64 +-------- .../storyteller_client/src/utils/api_host.rs | 10 +- .../src/utils/basic_json_post_request.rs | 71 ++++++++++ .../storyteller_client/src/utils/mod.rs | 1 + .../web/storyteller_web/src/docs/api_doc.rs | 4 +- .../endpoints/webhooks/fal_webhook_handler.rs | 4 +- .../webhooks/handle_images_payload.rs | 124 ++++++++++++++++++ .../src/http_server/endpoints/webhooks/mod.rs | 1 + .../app/src/pages/PageEnigma/PageEditor.tsx | 4 +- .../app/src/pages/PageVideo/ImageToVideo.tsx | 4 + frontend/libs/api/src/lib/enums/Api.ts | 2 + .../model-selector/src/lib/model-lists.tsx | 31 ++++- .../promptbox/src/lib/PromptBoxVideo.tsx | 2 + 32 files changed, 429 insertions(+), 277 deletions(-) create mode 100644 crates/desktop/artcraft/src/core/model/image_models.rs create mode 100644 crates/desktop/artcraft/src/core/model/mod.rs create mode 100644 crates/desktop/artcraft/src/core/model/video_models.rs create mode 100644 crates/desktop/artcraft/src/core/state/app_env_configs/app_env_configs.rs create mode 100644 crates/desktop/artcraft/src/core/state/app_env_configs/app_env_configs_serializeable.rs create mode 100644 crates/desktop/artcraft/src/core/state/app_env_configs/mod.rs create mode 100644 crates/lib/api_clients/storyteller_client/src/utils/basic_json_post_request.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/handle_images_payload.rs diff --git a/crates/desktop/artcraft/capabilities/default.json b/crates/desktop/artcraft/capabilities/default.json index 7e62e230b6..3192e556c2 100644 --- a/crates/desktop/artcraft/capabilities/default.json +++ b/crates/desktop/artcraft/capabilities/default.json @@ -16,6 +16,9 @@ "allow": [ { "url": "https://*" + }, + { + "url": "http://localhost:12345" } ] }, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image/enqueue_text_to_image_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image/enqueue_text_to_image_command.rs index 9ea18682a5..a89e5e507c 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image/enqueue_text_to_image_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image/enqueue_text_to_image_command.rs @@ -1,10 +1,13 @@ +use crate::core::commands::enqueue::image::handle_image_artcraft::handle_image_artcraft; use crate::core::commands::enqueue::image::handle_image_fal::handle_image_fal; use crate::core::commands::enqueue::image::handle_image_sora::handle_image_sora; use crate::core::commands::enqueue::image::internal_image_error::InternalImageError; +use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft; use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus}; use crate::core::commands::response::shorthand::Response; use crate::core::commands::response::success_response_wrapper::SerializeMarker; use crate::core::events::sendable_event_trait::SendableEvent; +use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; 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; @@ -55,8 +58,6 @@ use storyteller_client::media_files::upload_image_media_file_from_file::upload_i use storyteller_client::utils::api_host::ApiHost; use tauri::{AppHandle, Emitter, Manager, State}; use tempfile::NamedTempFile; -use crate::core::commands::enqueue::image::handle_image_artcraft::handle_image_artcraft; -use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft; #[derive(Deserialize)] pub struct EnqueueTextToImageRequest { @@ -114,6 +115,7 @@ pub async fn enqueue_text_to_image_command( app: AppHandle, request: EnqueueTextToImageRequest, app_data_root: State<'_, AppDataRoot>, + app_env_configs: State<'_, AppEnvConfigs>, fal_creds_manager: State<'_, FalCredentialManager>, storyteller_creds_manager: State<'_, StorytellerCredentialManager>, fal_task_queue: State<'_, FalTaskQueue>, @@ -129,6 +131,7 @@ pub async fn enqueue_text_to_image_command( &app_data_root, &fal_creds_manager, &storyteller_creds_manager, + &app_env_configs, &fal_task_queue, &sora_creds_manager, &sora_task_queue, @@ -176,6 +179,7 @@ pub async fn handle_request( app_data_root: &AppDataRoot, fal_creds_manager: &FalCredentialManager, storyteller_creds_manager: &StorytellerCredentialManager, + app_env_configs: &AppEnvConfigs, fal_task_queue: &FalTaskQueue, sora_creds_manager: &SoraCredentialManager, sora_task_queue: &SoraTaskQueue, @@ -197,7 +201,7 @@ pub async fn handle_request( if fal_creds_manager.has_apparent_api_token()? { handle_image_fal(&app, request, fal_creds_manager, fal_task_queue).await?; } else { - handle_image_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?; + handle_image_artcraft(request, &app, app_env_configs, app_data_root, storyteller_creds_manager).await?; } Ok(()) diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image/handle_image_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image/handle_image_artcraft.rs index 116621f899..a9569fe7b6 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image/handle_image_artcraft.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image/handle_image_artcraft.rs @@ -4,6 +4,7 @@ use crate::core::events::basic_sendable_event_trait::BasicSendableEvent; use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider}; use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent; use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent; +use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; use crate::core::state::data_dir::app_data_root::AppDataRoot; use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir; use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir; @@ -37,6 +38,7 @@ use tauri::{AppHandle, State}; pub async fn handle_image_artcraft( request: EnqueueTextToImageRequest, app: &AppHandle, + app_env_configs: &AppEnvConfigs, app_data_root: &AppDataRoot, storyteller_creds_manager: &StorytellerCredentialManager, ) -> Result<(), InternalImageError> { @@ -82,7 +84,7 @@ pub async fn handle_image_artcraft( num_images: None, }; let result = generate_flux_1_dev_text_to_image( - &ApiHost::Storyteller, + &app_env_configs.storyteller_host, Some(&creds), request, ).await; @@ -107,7 +109,7 @@ pub async fn handle_image_artcraft( num_images: None, }; let result = generate_flux_1_schnell_text_to_image( - &ApiHost::Storyteller, + &app_env_configs.storyteller_host, Some(&creds), request, ).await; @@ -132,7 +134,7 @@ pub async fn handle_image_artcraft( num_images: None, }; let result = generate_flux_pro_11_text_to_image( - &ApiHost::Storyteller, + &app_env_configs.storyteller_host, Some(&creds), request, ).await; @@ -160,7 +162,7 @@ pub async fn handle_image_artcraft( num_images: None, }; let result = generate_flux_pro_11_ultra_text_to_image( - &ApiHost::Storyteller, + &app_env_configs.storyteller_host, Some(&creds), request, ).await; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/video/enqueue_image_to_video_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/video/enqueue_image_to_video_command.rs index cb80436360..361214a652 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/video/enqueue_image_to_video_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/video/enqueue_image_to_video_command.rs @@ -7,6 +7,7 @@ use crate::core::commands::response::failure_response_wrapper::{CommandErrorResp use crate::core::commands::response::shorthand::Response; use crate::core::commands::response::success_response_wrapper::SerializeMarker; use crate::core::events::sendable_event_trait::SendableEvent; +use crate::core::model::video_models::VideoModel; 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; @@ -66,25 +67,7 @@ pub struct EnqueueImageToVideoRequest { pub image_media_token: Option, /// The model to use. - pub model: Option, -} - -#[derive(Deserialize, Debug, Copy, Clone)] -#[serde(rename_all = "snake_case")] -pub enum EnqueueImageToVideoModel { - #[deprecated(note = "Use `kling_1_6_pro` instead")] - #[serde(rename = "kling1_6")] - Kling16, - #[serde(rename = "kling_1_6_pro")] - Kling16Pro, - #[serde(rename = "kling_2_1_pro")] - Kling21Pro, - #[serde(rename = "kling_2_1_master")] - Kling21Master, - #[serde(rename = "seedance_1_0_lite")] - Seedance10Lite, - #[serde(rename = "veo_2")] - Veo2, + pub model: Option, } #[derive(Serialize)] diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/video/handle_video_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/video/handle_video_artcraft.rs index bc64caa1e8..de377ccef6 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/video/handle_video_artcraft.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/video/handle_video_artcraft.rs @@ -1,11 +1,12 @@ use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest}; use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest}; -use crate::core::commands::enqueue::video::enqueue_image_to_video_command::{EnqueueImageToVideoModel, EnqueueImageToVideoRequest}; +use crate::core::commands::enqueue::video::enqueue_image_to_video_command::EnqueueImageToVideoRequest; use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError; use crate::core::events::basic_sendable_event_trait::BasicSendableEvent; use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider}; use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent; use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent; +use crate::core::model::video_models::VideoModel; use crate::core::state::data_dir::app_data_root::AppDataRoot; use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir; use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir; @@ -17,15 +18,24 @@ use crate::services::storyteller::state::storyteller_credential_manager::Storyte use anyhow::anyhow; use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest; use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::{GenerateKling16ProAspectRatio, GenerateKling16ProImageToVideoRequest}; +use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::{GenerateKling21MasterAspectRatio, GenerateKling21MasterImageToVideoRequest}; +use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::{GenerateKling21ProAspectRatio, GenerateKling21ProImageToVideoRequest}; +use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoRequest; +use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::{GenerateVeo2AspectRatio, GenerateVeo2ImageToVideoRequest}; use fal_client::creds::fal_api_key::FalApiKey; use fal_client::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args}; use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs}; use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs}; use fal_client::requests::queue::video_gen::enqueue_kling_16_pro_image_to_video::Kling16ProAspectRatio; +use fal_client::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::Veo2AspectRatio; use idempotency::uuid::generate_random_uuid; use log::{error, info, warn}; use storyteller_client::generate::object::generate_hunyuan_3d_2_0_image_to_3d::generate_hunyuan3d_2_0_image_to_3d; use storyteller_client::generate::video::generate_kling_16_pro_image_to_video::generate_kling_16_pro_image_to_video; +use storyteller_client::generate::video::generate_kling_21_master_image_to_video::generate_kling_21_master_image_to_video; +use storyteller_client::generate::video::generate_kling_21_pro_image_to_video::generate_kling_21_pro_image_to_video; +use storyteller_client::generate::video::generate_seedance_1_0_lite_image_to_video::generate_seedance_1_0_lite_image_to_video; +use storyteller_client::generate::video::generate_veo_2_image_to_video::generate_veo_2_image_to_video; use storyteller_client::utils::api_host::ApiHost; use tauri::{AppHandle, State}; use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::{GenerateKling21MasterAspectRatio, GenerateKling21MasterImageToVideoRequest}; @@ -78,10 +88,7 @@ pub async fn handle_video_artcraft( None => { return Err(InternalVideoError::NoModelSpecified); } - Some( - EnqueueImageToVideoModel::Kling16 | - EnqueueImageToVideoModel::Kling16Pro - ) => { + Some(VideoModel::Kling16Pro) => { info!("enqueue Kling 1.6 Pro with Artcraft API"); selected_model = Some(GenerationModel::Kling1_6); let request = GenerateKling16ProImageToVideoRequest { @@ -107,7 +114,7 @@ pub async fn handle_video_artcraft( } } } - Some(EnqueueImageToVideoModel::Kling21Master) => { + Some(VideoModel::Kling21Master) => { info!("enqueue Kling 2.1 Master with Artcraft API"); selected_model = Some(GenerationModel::Kling21Master); let request = GenerateKling21MasterImageToVideoRequest { @@ -133,7 +140,8 @@ pub async fn handle_video_artcraft( } } } - Some(EnqueueImageToVideoModel::Kling21Pro) => { + Some(VideoModel::Kling21Pro) => { + info!("enqueue Kling 2.1 Pro with Artcraft API"); selected_model = Some(GenerationModel::Kling21Pro); let request = GenerateKling21ProImageToVideoRequest { @@ -159,7 +167,7 @@ pub async fn handle_video_artcraft( } } } - Some(EnqueueImageToVideoModel::Seedance10Lite) => { + Some(VideoModel::Seedance10Lite) => { info!("enqueue Seedance 1.0 Lite with Artcraft API"); selected_model = Some(GenerationModel::Seedance10Lite); let request = GenerateSeedance10LiteImageToVideoRequest { @@ -185,7 +193,7 @@ pub async fn handle_video_artcraft( } } } - Some(EnqueueImageToVideoModel::Veo2) => { + Some(VideoModel::Veo2) => { info!("enqueue Veo 2 with Artcraft API"); selected_model = Some(GenerationModel::Veo2); let request = GenerateVeo2ImageToVideoRequest { diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/video/handle_video_fal.rs b/crates/desktop/artcraft/src/core/commands/enqueue/video/handle_video_fal.rs index 4ce4aa1c4c..f7ecd0c4af 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/video/handle_video_fal.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/video/handle_video_fal.rs @@ -1,10 +1,11 @@ use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest}; -use crate::core::commands::enqueue::video::enqueue_image_to_video_command::{EnqueueImageToVideoModel, EnqueueImageToVideoRequest}; +use crate::core::commands::enqueue::video::enqueue_image_to_video_command::EnqueueImageToVideoRequest; use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError; use crate::core::events::basic_sendable_event_trait::BasicSendableEvent; use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider}; use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent; use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent; +use crate::core::model::video_models::VideoModel; use crate::core::state::data_dir::app_data_root::AppDataRoot; use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir; use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir; @@ -68,10 +69,7 @@ pub async fn handle_video_fal( None => { return Err(InternalVideoError::NoModelSpecified); } - Some( - EnqueueImageToVideoModel::Kling16 | - EnqueueImageToVideoModel::Kling16Pro - ) => { + Some(VideoModel::Kling16Pro) => { info!("enqueue Kling 1.6 image to video with Kling API"); selected_model = Some(GenerationModel::Kling1_6); enqueue_kling_16_pro_image_to_video(Kling16ProArgs { diff --git a/crates/desktop/artcraft/src/core/mod.rs b/crates/desktop/artcraft/src/core/mod.rs index 208ed1126b..e4b36574bf 100644 --- a/crates/desktop/artcraft/src/core/mod.rs +++ b/crates/desktop/artcraft/src/core/mod.rs @@ -1,6 +1,7 @@ pub mod artcraft_error; pub mod commands; pub mod events; +pub mod model; pub mod state; pub mod threads; pub mod utils; diff --git a/crates/desktop/artcraft/src/core/model/image_models.rs b/crates/desktop/artcraft/src/core/model/image_models.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/desktop/artcraft/src/core/model/mod.rs b/crates/desktop/artcraft/src/core/model/mod.rs new file mode 100644 index 0000000000..7ee84bd8bc --- /dev/null +++ b/crates/desktop/artcraft/src/core/model/mod.rs @@ -0,0 +1,2 @@ +pub mod image_models; +pub mod video_models; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/core/model/video_models.rs b/crates/desktop/artcraft/src/core/model/video_models.rs new file mode 100644 index 0000000000..7e2aadb784 --- /dev/null +++ b/crates/desktop/artcraft/src/core/model/video_models.rs @@ -0,0 +1,22 @@ +use serde_derive::Deserialize; + +/// This is used in the Tauri command bridge. +/// Don't change the serializations without coordinating with the frontend. +#[derive(Deserialize, Debug, Copy, Clone)] +#[serde(rename_all = "snake_case")] +pub enum VideoModel { + #[serde(rename = "kling_1.6_pro")] + Kling16Pro, + + #[serde(rename = "kling_2.1_pro")] + Kling21Pro, + + #[serde(rename = "kling_2.1_master")] + Kling21Master, + + #[serde(rename = "seedance_1.0_lite")] + Seedance10Lite, + + #[serde(rename = "veo_2")] + Veo2, +} diff --git a/crates/desktop/artcraft/src/core/state/app_env_configs/app_env_configs.rs b/crates/desktop/artcraft/src/core/state/app_env_configs/app_env_configs.rs new file mode 100644 index 0000000000..2e6b21a940 --- /dev/null +++ b/crates/desktop/artcraft/src/core/state/app_env_configs/app_env_configs.rs @@ -0,0 +1,38 @@ +use log::info; +use errors::AnyhowResult; +use storyteller_client::utils::api_host::ApiHost; +use crate::core::state::app_env_configs::app_env_configs_serializeable::{AppEnvConfigsSerializable, StorytellerApiHost}; +use crate::core::state::data_dir::app_data_root::AppDataRoot; + +#[derive(Clone)] +pub struct AppEnvConfigs { + pub storyteller_host: ApiHost, +} + +impl AppEnvConfigs { + + pub fn load_from_filesystem(root: &AppDataRoot) -> AnyhowResult { + println!("Loading app environmental configs from filesystem..."); + let input = AppEnvConfigsSerializable::load_from_filesystem(root)?; + + let storyteller = input.as_ref() + .map(|i| i.storyteller_host) + .flatten(); + + let storyteller_api = match storyteller { + Some(StorytellerApiHost::Localhost) => ApiHost::Localhost { + port: input.as_ref() + .and_then(|i| i.storyteller_port) + .unwrap_or(12345) + }, + Some(StorytellerApiHost::Production) => ApiHost::Storyteller, + _ => ApiHost::Storyteller, + }; + + println!("Using storyteller API host: {:?}", storyteller_api); + + Ok(Self { + storyteller_host: storyteller_api, + }) + } +} diff --git a/crates/desktop/artcraft/src/core/state/app_env_configs/app_env_configs_serializeable.rs b/crates/desktop/artcraft/src/core/state/app_env_configs/app_env_configs_serializeable.rs new file mode 100644 index 0000000000..483003d9b5 --- /dev/null +++ b/crates/desktop/artcraft/src/core/state/app_env_configs/app_env_configs_serializeable.rs @@ -0,0 +1,34 @@ +use serde_derive::{Deserialize, Serialize}; +use errors::AnyhowResult; +use crate::core::state::data_dir::app_data_root::AppDataRoot; + +const CURRENT_VERSION: &str = "1"; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum StorytellerApiHost { + Production, + Localhost, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct AppEnvConfigsSerializable { + /// Versioning string. + pub version: Option, + pub storyteller_port: Option, + pub storyteller_host: Option, +} + +impl AppEnvConfigsSerializable { + pub fn load_from_filesystem(app_data_root: &AppDataRoot) -> AnyhowResult> { + let filename = app_data_root.settings_dir().get_app_env_configs_path(); + if !filename.exists() { + return Ok(None); + } + + let contents = std::fs::read_to_string(filename)?; + let data: Self = serde_json::from_str(&contents)?; + Ok(Some(data)) + } +} diff --git a/crates/desktop/artcraft/src/core/state/app_env_configs/mod.rs b/crates/desktop/artcraft/src/core/state/app_env_configs/mod.rs new file mode 100644 index 0000000000..be171e276d --- /dev/null +++ b/crates/desktop/artcraft/src/core/state/app_env_configs/mod.rs @@ -0,0 +1,2 @@ +pub mod app_env_configs; +pub mod app_env_configs_serializeable; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/core/state/data_dir/app_settings_dir.rs b/crates/desktop/artcraft/src/core/state/data_dir/app_settings_dir.rs index e3a48b21ef..59cc921555 100644 --- a/crates/desktop/artcraft/src/core/state/data_dir/app_settings_dir.rs +++ b/crates/desktop/artcraft/src/core/state/data_dir/app_settings_dir.rs @@ -31,7 +31,11 @@ impl AppSettingsDir { pub fn get_app_preferences_path(&self) -> PathBuf { self.path.join("app_preferences.json") } - + + pub fn get_app_env_configs_path(&self) -> PathBuf { + self.path.join("env_configs.json") + } + pub fn write_app_preferences(&self, prefs: &AppPreferences) -> AnyhowResult<()> { let serializable = prefs.to_serializable(); diff --git a/crates/desktop/artcraft/src/core/state/mod.rs b/crates/desktop/artcraft/src/core/state/mod.rs index beda27a3c3..9b2f65bd9b 100644 --- a/crates/desktop/artcraft/src/core/state/mod.rs +++ b/crates/desktop/artcraft/src/core/state/mod.rs @@ -1,3 +1,4 @@ +pub mod app_env_configs; pub mod app_preferences; pub mod app_startup_time; pub mod data_dir; diff --git a/crates/desktop/artcraft/src/lib.rs b/crates/desktop/artcraft/src/lib.rs index e7791f5ccf..88f4a593b7 100644 --- a/crates/desktop/artcraft/src/lib.rs +++ b/crates/desktop/artcraft/src/lib.rs @@ -11,6 +11,7 @@ use crate::core::commands::enqueue::video::enqueue_image_to_video_command::enque use crate::core::commands::flip_image::flip_image; use crate::core::commands::get_build_info_command::get_build_info_command; use crate::core::commands::platform_info_command::platform_info_command; +use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; 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; @@ -71,6 +72,9 @@ pub fn run() { let fal_task_queue = FalTaskQueue::new(); let fal_task_queue_2 = fal_task_queue.clone(); + + let app_env_configs = AppEnvConfigs::load_from_filesystem(&app_data_root) + .expect("AppEnvConfigs should be loaded from disk"); println!("Initializing backend runtime..."); @@ -155,6 +159,7 @@ pub fn run() { Ok(()) }) .manage(app_data_root) + .manage(app_env_configs) .manage(app_preferences) .manage(fal_creds_manager) .manage(fal_task_queue) diff --git a/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_1_dev_text_to_image.rs b/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_1_dev_text_to_image.rs index 0e8280983f..9257f811c9 100644 --- a/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_1_dev_text_to_image.rs +++ b/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_1_dev_text_to_image.rs @@ -1,68 +1,19 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; -use crate::error::api_error::ApiError; -use crate::error::client_error::ClientError; use crate::error::storyteller_error::StorytellerError; 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::basic_json_post_request::basic_json_post_request; use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageRequest, GenerateFlux1DevTextToImageResponse, GENERATE_FLUX_1_DEV_TEXT_TO_IMAGE_PATH}; -use log::{debug, info}; -use reqwest::Client; + pub async fn generate_flux_1_dev_text_to_image( api_host: &ApiHost, maybe_creds: Option<&StorytellerCredentialSet>, request: GenerateFlux1DevTextToImageRequest, ) -> Result { - - let url = get_route(api_host); - - info!("Requesting {:?}", &url); - - let client = Client::builder() - .gzip(true) - .build() - .map_err(|err| StorytellerError::Client(ClientError::from(err)))?; - - let mut request_builder = client.post(url) - .header("User-Agent", USER_AGENT) - .header("Accept", APPLICATION_JSON) - .header("Content-Type", 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 request_body = serde_json::to_string(&request) - .map_err(|err| StorytellerError::Client(ClientError::from(err)))?; - - debug!("Request body: {:?}", request_body); - - let response = request_builder - .body(request_body) - .send() - .await - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - let response = filter_bad_response(response) - .await - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - let response_body = &response.text() - .await - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - debug!("Response body: {:?}", response_body); - - let response = serde_json::from_str(&response_body) - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - Ok(response) -} - -fn get_route(api_host: &ApiHost) -> String { - let api_hostname = api_host.to_api_hostname(); - format!("https://{}{}", api_hostname, GENERATE_FLUX_1_DEV_TEXT_TO_IMAGE_PATH) + Ok(basic_json_post_request( + api_host, + GENERATE_FLUX_1_DEV_TEXT_TO_IMAGE_PATH, + maybe_creds, + request, + ).await?) } diff --git a/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_1_schnell_text_to_image.rs b/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_1_schnell_text_to_image.rs index 2b58b2b32a..a5f7b5cf15 100644 --- a/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_1_schnell_text_to_image.rs +++ b/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_1_schnell_text_to_image.rs @@ -1,68 +1,19 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; -use crate::error::api_error::ApiError; -use crate::error::client_error::ClientError; use crate::error::storyteller_error::StorytellerError; 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::basic_json_post_request::basic_json_post_request; use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH}; -use log::{debug, info}; -use reqwest::Client; + pub async fn generate_flux_1_schnell_text_to_image( api_host: &ApiHost, maybe_creds: Option<&StorytellerCredentialSet>, request: GenerateFlux1SchnellTextToImageRequest, ) -> Result { - - let url = get_route(api_host); - - info!("Requesting {:?}", &url); - - let client = Client::builder() - .gzip(true) - .build() - .map_err(|err| StorytellerError::Client(ClientError::from(err)))?; - - let mut request_builder = client.post(url) - .header("User-Agent", USER_AGENT) - .header("Accept", APPLICATION_JSON) - .header("Content-Type", 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 request_body = serde_json::to_string(&request) - .map_err(|err| StorytellerError::Client(ClientError::from(err)))?; - - debug!("Request body: {:?}", request_body); - - let response = request_builder - .body(request_body) - .send() - .await - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - let response = filter_bad_response(response) - .await - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - let response_body = &response.text() - .await - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - debug!("Response body: {:?}", response_body); - - let response = serde_json::from_str(&response_body) - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - Ok(response) -} - -fn get_route(api_host: &ApiHost) -> String { - let api_hostname = api_host.to_api_hostname(); - format!("https://{}{}", api_hostname, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH) + Ok(basic_json_post_request( + api_host, + GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH, + maybe_creds, + request, + ).await?) } diff --git a/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_pro_11_text_to_image.rs b/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_pro_11_text_to_image.rs index 250e7ab550..8a5196d442 100644 --- a/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_pro_11_text_to_image.rs +++ b/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_pro_11_text_to_image.rs @@ -1,68 +1,19 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; -use crate::error::api_error::ApiError; -use crate::error::client_error::ClientError; use crate::error::storyteller_error::StorytellerError; 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::basic_json_post_request::basic_json_post_request; use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageRequest, GenerateFluxPro11TextToImageResponse, GENERATE_FLUX_PRO_11_TEXT_TO_IMAGE_PATH}; -use log::{debug, info}; -use reqwest::Client; + pub async fn generate_flux_pro_11_text_to_image( api_host: &ApiHost, maybe_creds: Option<&StorytellerCredentialSet>, request: GenerateFluxPro11TextToImageRequest, ) -> Result { - - let url = get_route(api_host); - - info!("Requesting {:?}", &url); - - let client = Client::builder() - .gzip(true) - .build() - .map_err(|err| StorytellerError::Client(ClientError::from(err)))?; - - let mut request_builder = client.post(url) - .header("User-Agent", USER_AGENT) - .header("Accept", APPLICATION_JSON) - .header("Content-Type", 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 request_body = serde_json::to_string(&request) - .map_err(|err| StorytellerError::Client(ClientError::from(err)))?; - - debug!("Request body: {:?}", request_body); - - let response = request_builder - .body(request_body) - .send() - .await - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - let response = filter_bad_response(response) - .await - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - let response_body = &response.text() - .await - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - debug!("Response body: {:?}", response_body); - - let response = serde_json::from_str(&response_body) - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - Ok(response) -} - -fn get_route(api_host: &ApiHost) -> String { - let api_hostname = api_host.to_api_hostname(); - format!("https://{}{}", api_hostname, GENERATE_FLUX_PRO_11_TEXT_TO_IMAGE_PATH) + Ok(basic_json_post_request( + api_host, + GENERATE_FLUX_PRO_11_TEXT_TO_IMAGE_PATH, + maybe_creds, + request, + ).await?) } diff --git a/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_pro_11_ultra_text_to_image.rs b/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_pro_11_ultra_text_to_image.rs index dc8a25ad99..6ba10770be 100644 --- a/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_pro_11_ultra_text_to_image.rs +++ b/crates/lib/api_clients/storyteller_client/src/generate/image/generate_flux_pro_11_ultra_text_to_image.rs @@ -1,68 +1,18 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; -use crate::error::api_error::ApiError; -use crate::error::client_error::ClientError; use crate::error::storyteller_error::StorytellerError; 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::basic_json_post_request::basic_json_post_request; use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageRequest, GenerateFluxPro11UltraTextToImageResponse, GENERATE_FLUX_PRO_11_ULTRA_TEXT_TO_IMAGE_PATH}; -use log::{debug, info}; -use reqwest::Client; pub async fn generate_flux_pro_11_ultra_text_to_image( api_host: &ApiHost, maybe_creds: Option<&StorytellerCredentialSet>, request: GenerateFluxPro11UltraTextToImageRequest, ) -> Result { - - let url = get_route(api_host); - - info!("Requesting {:?}", &url); - - let client = Client::builder() - .gzip(true) - .build() - .map_err(|err| StorytellerError::Client(ClientError::from(err)))?; - - let mut request_builder = client.post(url) - .header("User-Agent", USER_AGENT) - .header("Accept", APPLICATION_JSON) - .header("Content-Type", 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 request_body = serde_json::to_string(&request) - .map_err(|err| StorytellerError::Client(ClientError::from(err)))?; - - debug!("Request body: {:?}", request_body); - - let response = request_builder - .body(request_body) - .send() - .await - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - let response = filter_bad_response(response) - .await - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - let response_body = &response.text() - .await - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - debug!("Response body: {:?}", response_body); - - let response = serde_json::from_str(&response_body) - .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - - Ok(response) -} - -fn get_route(api_host: &ApiHost) -> String { - let api_hostname = api_host.to_api_hostname(); - format!("https://{}{}", api_hostname, GENERATE_FLUX_PRO_11_ULTRA_TEXT_TO_IMAGE_PATH) + Ok(basic_json_post_request( + api_host, + GENERATE_FLUX_PRO_11_ULTRA_TEXT_TO_IMAGE_PATH, + maybe_creds, + request, + ).await?) } diff --git a/crates/lib/api_clients/storyteller_client/src/utils/api_host.rs b/crates/lib/api_clients/storyteller_client/src/utils/api_host.rs index cc75a97538..ea45493b82 100644 --- a/crates/lib/api_clients/storyteller_client/src/utils/api_host.rs +++ b/crates/lib/api_clients/storyteller_client/src/utils/api_host.rs @@ -1,5 +1,5 @@ -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum ApiHost { Storyteller, FakeYou, @@ -14,4 +14,12 @@ impl ApiHost { ApiHost::Localhost { port } => format!("localhost:{}", port), } } + + pub fn to_api_hostname_and_scheme(&self) -> String { + match self { + ApiHost::Storyteller => "https://api.storyteller.ai".to_string(), + ApiHost::FakeYou => "https://api.fakeyou.com".to_string(), + ApiHost::Localhost { port } => format!("http://localhost:{}", port), + } + } } diff --git a/crates/lib/api_clients/storyteller_client/src/utils/basic_json_post_request.rs b/crates/lib/api_clients/storyteller_client/src/utils/basic_json_post_request.rs new file mode 100644 index 0000000000..838e97b14c --- /dev/null +++ b/crates/lib/api_clients/storyteller_client/src/utils/basic_json_post_request.rs @@ -0,0 +1,71 @@ +use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; +use crate::error::api_error::ApiError; +use crate::error::client_error::ClientError; +use crate::error::storyteller_error::StorytellerError; +use crate::utils::api_host::ApiHost; +use crate::utils::constants::{APPLICATION_JSON, USER_AGENT}; +use crate::utils::filter_bad_response::filter_bad_response; +use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH}; +use log::{debug, info}; +use reqwest::Client; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +pub async fn basic_json_post_request( + api_host: &ApiHost, + route_path: &str, + maybe_creds: Option<&StorytellerCredentialSet>, + request: Req, +) -> Result { + + let url = get_route(api_host, route_path); + + info!("Requesting {:?}", &url); + + let client = Client::builder() + .gzip(true) + .build() + .map_err(|err| StorytellerError::Client(ClientError::from(err)))?; + + let mut request_builder = client.post(url) + .header("User-Agent", USER_AGENT) + .header("Accept", APPLICATION_JSON) + .header("Content-Type", 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 request_body = serde_json::to_string(&request) + .map_err(|err| StorytellerError::Client(ClientError::from(err)))?; + + debug!("Request body: {:?}", request_body); + + let response = request_builder + .body(request_body) + .send() + .await + .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; + + let response = filter_bad_response(response) + .await + .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; + + let response_body = &response.text() + .await + .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; + + debug!("Response body: {:?}", response_body); + + let response = serde_json::from_str(&response_body) + .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; + + Ok(response) +} + +fn get_route(api_host: &ApiHost, route_path: &str) -> String { + let api_hostname = api_host.to_api_hostname_and_scheme(); + format!("{}{}", api_hostname, route_path) +} diff --git a/crates/lib/api_clients/storyteller_client/src/utils/mod.rs b/crates/lib/api_clients/storyteller_client/src/utils/mod.rs index a9fc1c4753..defbbbc4c5 100644 --- a/crates/lib/api_clients/storyteller_client/src/utils/mod.rs +++ b/crates/lib/api_clients/storyteller_client/src/utils/mod.rs @@ -1,4 +1,5 @@ pub mod api_host; +pub mod basic_json_post_request; pub mod constants; pub mod filter_bad_response; pub mod http_get_anonymous; diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index 01184a7ca8..ae7270a2d8 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -527,8 +527,8 @@ use crate::http_server::web_utils::response_success_helpers::*; GenerateFluxPro11UltraTextToImageResponse, GenerateHunyuan21ImageTo3dRequest, GenerateHunyuan21ImageTo3dResponse, - GenerateHunyuan2ImageTo3dRequest, - GenerateHunyuan2ImageTo3dResponse, + GenerateHunyuan20ImageTo3dRequest, + GenerateHunyuan20ImageTo3dResponse, GenerateKling16ProAspectRatio, GenerateKling16ProDuration, GenerateKling16ProImageToVideoRequest, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal_webhook_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal_webhook_handler.rs index 38fb916a82..992e714107 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal_webhook_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal_webhook_handler.rs @@ -2,6 +2,7 @@ use std::fmt; use std::sync::Arc; use crate::http_server::endpoints::webhooks::handle_image_payload::handle_image_payload; +use crate::http_server::endpoints::webhooks::handle_images_payload::handle_images_payload; use crate::http_server::endpoints::webhooks::handle_model_mesh_payload::handle_model_mesh_payload; use crate::http_server::endpoints::webhooks::handle_video_payload::handle_video_payload; use crate::state::server_state::ServerState; @@ -18,7 +19,6 @@ use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id: use mysql_queries::queries::generic_inference::fal::mark_fal_generic_inference_job_successfully_done::mark_fal_generic_inference_job_successfully_done; use serde_json::Value; use utoipa::ToSchema; - // 1. tauri --> hit endpoint to enqueue // // 2. webhook @@ -150,6 +150,8 @@ pub async fn fal_webhook_handler( info!("Handling image payload for job: {:?}", job.job_token); let token = handle_image_payload(payload_obj, &job, &server_state).await?; maybe_media_token = Some(token); + } else if payload_obj.contains_key("images") { + maybe_media_token = handle_images_payload(payload_obj, &job, &server_state).await?; } else if payload_obj.contains_key("video") { info!("Handling video payload for job: {:?}", job.job_token); let token = handle_video_payload(payload_obj, &job, &server_state).await?; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/handle_images_payload.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/handle_images_payload.rs new file mode 100644 index 0000000000..1e23305fd2 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/handle_images_payload.rs @@ -0,0 +1,124 @@ +use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError; +use crate::state::server_state::ServerState; +use crate::util::http_download_url_to_bytes::http_download_url_to_bytes; +use crate::util::http_download_url_to_tempfile::http_download_url_to_tempfile; +use actix_web::web; +use anyhow::anyhow; +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::by_table::media_files::media_file_class::MediaFileClass; +use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory; +use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory; +use enums::by_table::media_files::media_file_type::MediaFileType; +use errors::AnyhowResult; +use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes; +use images::image_info::image_info::ImageInfo; +use log::{info, warn}; +use mimetypes::mimetype_info::mimetype_info::MimetypeInfo; +use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails; +use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder; +use serde_json::{Map, Value}; +use std::sync::Arc; +use tokens::tokens::media_files::MediaFileToken; + +const PREFIX : Option<&str> = Some("artcraft_"); + +#[derive(Deserialize, Debug)] +pub struct FalWebhookImage { + pub content_type: Option, + pub file_name: Option, + pub file_size: Option, + pub height: Option, + pub width: Option, + pub url: Option, +} + +pub async fn handle_images_payload( + payload: &Map, + job: &FalJobDetails, + server_state: &ServerState, +) -> AnyhowResult> { + + let images_value = payload.get("images") + .ok_or_else(|| anyhow!("no `images` key in payload"))?; + + info!("Fal Images Payload: {:?}", images_value); + + let images: Vec = serde_json::from_value(images_value.clone())?; + + let mut maybe_media_token = None; + + for image in images { + let media_token = upload_image(job, server_state, &image).await?; + + if maybe_media_token.is_none() { + maybe_media_token = Some(media_token); // Set the first media token + } + } + + Ok(maybe_media_token) +} + +pub async fn upload_image( + job: &FalJobDetails, + server_state: &ServerState, + image: &FalWebhookImage, +) -> AnyhowResult { + let image_url = image.url + .as_deref() + .ok_or_else(|| anyhow!("no `url` in image payload"))?; + + //let mime_type = image.content_type + // .as_deref() + // .ok_or_else(|| anyhow!("no `content_type` in image payload"))?; + + let file_bytes = http_download_url_to_bytes(image_url) + .await + .map_err(|e| anyhow!("Failed to download image: {:?}", e))?; + + let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes) + .ok_or_else(|| anyhow!("Failed to get mimetype info"))?; + + let mime_type = mimetype_info.mime_type(); + + let media_file_type = MediaFileType::try_from_mime_type(mime_type) + .ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?; + + let extension_with_period = mimetype_info.file_extension() + .map(|ext| ext.extension_with_period()) + .ok_or_else(|| anyhow!("Failed to get file extension from mimetype info"))?; + + let file_size_bytes = file_bytes.len(); + let file_hash = sha256_hash_bytes(&file_bytes)?; + let image_info = ImageInfo::decode_image_from_bytes(&file_bytes)?; + + let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period)); + + info!("Uploading media to bucket path: {}", public_upload_path.get_full_object_path_str()); + + server_state.public_bucket_client.upload_file_with_content_type_process( + public_upload_path.get_full_object_path_str(), + file_bytes.as_ref(), + &mime_type) + .await?; + + let media_token = MediaFileInsertBuilder::new() + .maybe_creator_user(job.maybe_creator_user_token.as_ref()) + .maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref()) + .creator_ip_address(&job.creator_ip_address) + .public_bucket_directory_hash(&public_upload_path) + .media_file_class(MediaFileClass::Image) + .media_file_type(media_file_type) + .media_file_origin_category(MediaFileOriginCategory::Inference) + //.media_file_origin_product_category(MediaFileOriginProductCategory::Unknown) + .mime_type(mime_type) + .file_size_bytes(file_size_bytes as u64) + .frame_width(image_info.width()) + .frame_height(image_info.height()) + .checksum_sha2(&file_hash) + .insert_pool(&server_state.mysql_pool) + .await?; + + info!("Image media file uploaded with token: {}", media_token); + + Ok(media_token) +} \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/mod.rs index 49cfe089d9..f1e424f5f2 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/mod.rs @@ -1,4 +1,5 @@ pub mod fal_webhook_handler; pub mod handle_image_payload; +pub mod handle_images_payload; pub mod handle_model_mesh_payload; pub mod handle_video_payload; \ No newline at end of file diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx b/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx index 24532cb96b..46fec9ce4f 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx @@ -527,7 +527,7 @@ export const PageEditor = () => { )} {tabStore.activeTabId == "IMAGE" && (
- @@ -535,7 +535,7 @@ export const PageEditor = () => { )} {tabStore.activeTabId == "VIDEO" && (
- diff --git a/frontend/apps/editor3d/app/src/pages/PageVideo/ImageToVideo.tsx b/frontend/apps/editor3d/app/src/pages/PageVideo/ImageToVideo.tsx index d338db554a..70b46e5c81 100644 --- a/frontend/apps/editor3d/app/src/pages/PageVideo/ImageToVideo.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageVideo/ImageToVideo.tsx @@ -21,6 +21,10 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => { selectedModels[ModelCategory.ImageToVideo] || videoGenerationModels[0]?.label; + console.log("Selected model:", selectedModel) + console.log("Image Media Id:", imageMediaId) + console.log("Image URL:", imageUrl) + const jobContext: JobContextType = { jobTokens: [], addJobToken: () => {}, diff --git a/frontend/libs/api/src/lib/enums/Api.ts b/frontend/libs/api/src/lib/enums/Api.ts index 302021939d..bee0c5cefb 100644 --- a/frontend/libs/api/src/lib/enums/Api.ts +++ b/frontend/libs/api/src/lib/enums/Api.ts @@ -1,4 +1,6 @@ export enum API_TARGETS { + // NB: Comment this out for quick testing in development + //BASE_API = "http://localhost:12345", BASE_API = "https://api.storyteller.ai", GOOGLE_API = "https://storage.googleapis.com", FUNNEL_API = "https://style.storyteller.ai", diff --git a/frontend/libs/components/model-selector/src/lib/model-lists.tsx b/frontend/libs/components/model-selector/src/lib/model-lists.tsx index 9d40ac57ec..a65e97a4f3 100644 --- a/frontend/libs/components/model-selector/src/lib/model-lists.tsx +++ b/frontend/libs/components/model-selector/src/lib/model-lists.tsx @@ -7,13 +7,36 @@ export type ModelList = Omit[]; export const allModels = { video: [ { - label: "Kling 2.0", + id: "kling_1.6_pro", + label: "Kling 1.6 Pro", + icon: , + description: "Good quality model", + badges: [{ label: "2 min.", icon: }], + }, + { + id: "kling_2.1_pro", + label: "Kling 2.1 Pro", icon: , description: "High quality model", badges: [{ label: "2 min.", icon: }], }, { - label: "Runway", + id: "kling_2.1_master", + label: "Kling 2.1 Master", + icon: , + description: "Master quality model ($$)", + badges: [{ label: "2 min.", icon: }], + }, + { + id: "seedance_1.0_lite", + label: "Seedance 1.0 Lite", + icon: , + description: "Fast and high-quality model", + badges: [{ label: "2 min.", icon: }], + }, + { + id: "veo_2", + label: "Google Veo 2", icon: , description: "Fast and high-quality model", badges: [{ label: "2 min.", icon: }], @@ -21,24 +44,28 @@ export const allModels = { ], image: [ { + id: "todo", label: "GPT Image 1 (GPT-4o)", icon: , description: "Slow, ultra instructive model", badges: [{ label: "45 sec.", icon: }], }, { + id: "todo", label: "Flux Pro Ultra", icon: , description: "High quality model", badges: [{ label: "15 sec.", icon: }], }, { + id: "todo", label: "Recraft 3", icon: , description: "Fast and high-quality model", badges: [{ label: "15 sec.", icon: }], }, { + id: "todo", label: "Flux.1 Kontext", icon: , description: "New model with advanced context", diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx index 9f1f680a39..6fd173dadb 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx @@ -183,6 +183,8 @@ export const PromptBoxVideo = ({ const handleEnqueue = async () => { setIsEnqueueing(true); + console.log("Handle Enqueue"); + setTimeout(() => { // TODO(bt,2025-05-08): This is a hack so we don't accidentally wind up with a permanently disabled prompt box if // the backend hangs on a given request.