Compare commits

...
Author SHA1 Message Date
Tanish Manku 65145f38e3 artcraft: install macOS app menu so Cmd+A/C/V/X/Z work in text inputs
Tauri 2 dropped the implicit default menu on macOS, so without an explicit
Edit submenu AppKit had nothing to dispatch the standard editing
accelerators through and Cmd+letter shortcuts never reached the WKWebView.
This was visible in the PageDraw text-to-image promptbox (Cmd+A would not
select all, etc.).

Install a minimal App/Edit/Window menu using PredefinedMenuItem on macOS
only; gated behind cfg(target_os = "macos") so Windows/Linux still have
no menu bar.
2026-05-05 16:20:14 -07:00
Tanish Manku 3e386a9127 artcraft: land on previous batch's last image when arrow-key paging left
PageDraw lightbox left-arrow at a batch boundary called openBatchInLightbox
without a startIndex, defaulting to 0, so leftward traversal skipped every
non-first image in each prior batch.
2026-05-05 16:07:27 -07:00
Tanish Manku 036aa3545f artcraft: clarify batch-pending dismissal with striped overlay and centered control
The per-tile X button suggested individual tiles could be dismissed, but a
batch resolves as a unit. The whole batch group is now the click target and
the hover overlay paints diagonal red/black stripes across all tiles so the
removal scope is unambiguous. Centered trash chip with a ×N badge for
count > 1 confirms the batch size at a glance.
2026-05-05 16:07:27 -07:00
Tanish Manku 7f5e766358 artcraft: dismissable pending tiles in PageDraw history stack
Pending tiles now have a hover-revealed trash button that opens a confirm
modal noting the image will still appear in the library when complete.
Removing the entry from pendingGenerations is enough — the Tauri completion
bridge filters incoming events on membership, so the eventual result is
silently dropped from PageDraw.

The confirm modal auto-closes if the underlying generation resolves before
the user confirms, and clears its tracked target if dismissed externally.
2026-05-05 16:07:27 -07:00
Tanish Manku d6d5d871e3 artcraft: port website Generation Details to lightbox-modal
Add Aspect Ratio, Resolution, Duration, Audio, Size, and Created rows to the
desktop app's lightbox so it matches the website's Information panel. Promote
the website's local format helpers into @storyteller/model-list so both
clients can share them.
2026-05-05 16:07:27 -07:00
Tanish Manku cdd76c0106 artcraft: resolve PageDraw pending tiles via text_to_image_generation_complete_event
PageDraw was listening for image_edit_complete_event, but the unified
generate_image Tauri command emits text_to_image_generation_complete_event
for all paths (text, edit, inpaint). Switch to that event and filter on
frontend_subscriber_id against pendingGenerations so each page only handles
its own results, mirroring the PageAngles/AnglesStore pattern.
2026-05-05 16:07:27 -07:00
Brandon Thomas 01854c394f move old tauri commands to "deprecated" 2026-05-03 19:36:20 -04:00
Brandon Thomas f9f8bba457 fix flux kontext 2026-05-03 16:57:21 -04:00
Brandon Thomas 3e9f9caf52 fix prompt refs 2026-05-03 14:22:53 -04:00
Brandon Thomas bd0acd05de fixup 2026-05-03 05:24:39 -04:00
Brandon Thomas a28325534d special model handling 2026-05-03 05:10:01 -04:00
Brandon Thomas 9b69a92a87 cleanup session management 2026-05-03 02:50:46 -04:00
Brandon Thomas 1673fef867 cleanup 2026-05-03 02:44:13 -04:00
Brandon Thomas 01dcf3b985 pull scene / canvas / mask tokens 2026-05-03 02:40:22 -04:00
Brandon Thomas 59ff4dc9b8 upload bytes to media file 2026-05-03 02:00:11 -04:00
Brandon Thomas ba5eca0195 cleanup 2026-05-03 00:59:25 -04:00
Brandon Thomas a77289c971 fix routing 2026-05-03 00:52:56 -04:00
Brandon Thomas 52b89565ca refactor model mapping 2026-05-03 00:43:21 -04:00
Brandon Thomas 742e00a052 Merge remote-tracking branch 'origin/bt-tauri-refactor' into bt-tauri-refactor 2026-05-03 00:29:05 -04:00
Tanish Manku c6515298a4 artcraft: migrate PromptBox3D off EnqueueEditImage onto unified GenerateImage
Last remaining legacy `Enqueue*` consumer in the artcraft Tauri app's
generation surface. Maps `image_count` -> `batch_size`, `disable_system_prompt`
-> `enable_system_prompt`, legacy aspect_ratio/image_resolution enums onto
CommonAspectRatio/CommonResolution. Folds the conditional common_aspect_ratio
branch into a single aspect_ratio field that prefers commonAspectRatio when
the model supports it.
2026-05-03 00:28:59 -04:00
Brandon Thomas 282a0e6a01 wip 2026-05-03 00:28:59 -04:00
Brandon Thomas 236474f6b4 cleanup 2026-05-03 00:28:59 -04:00
Brandon Thomas 255eeca554 get rid of older providers for now 2026-05-03 00:28:59 -04:00
Brandon Thomas 92630dde77 refactor, move handlers 2026-05-03 00:28:59 -04:00
Brandon Thomas 0bfe0f2a2f artcraft provider wip 2026-05-03 00:28:59 -04:00
Brandon Thomas a3bb3d44ce note 2026-05-03 00:28:59 -04:00
Brandon Thomas 5f0793d41e refactor wip 2026-05-03 00:28:59 -04:00
Tanish Manku 94846b580b artcraft: migrate frontend image/video calls to unified generate_* tauri commands
Add GenerateImage / GenerateVideo wrappers in @storyteller/tauri-api
that mirror the unified TauriGenerateImageRequest / TauriGenerateVideoRequest
shapes and invoke generate_image_command / generate_video_command. Migrate
PromptBoxImage (text-to-image), PromptBoxVideo (image-to-video), PageDraw
(edit + inpaint), and Angles off the per-flow Enqueue* wrappers onto the
unified ones. Old wrappers stay; Rust proxy handles routing.
2026-05-03 00:28:58 -04:00
Brandon Thomas cf091fbc4d proxy to other endpoints 2026-05-03 00:28:58 -04:00
Brandon Thomas 76156ecb8f stub "generate_image" endpoint 2026-05-03 00:28:58 -04:00
Brandon Thomas 954784d9a9 fix compile error, remove dead code 2026-05-03 00:28:58 -04:00
Brandon Thomas 8b9d34ce2d fix requests 2026-05-03 00:28:58 -04:00
Brandon Thomas a75052ac44 rename generate video command 2026-05-03 00:28:58 -04:00
Tanish Manku ccd7b38760 artcraft: migrate PromptBox3D off EnqueueEditImage onto unified GenerateImage
Last remaining legacy `Enqueue*` consumer in the artcraft Tauri app's
generation surface. Maps `image_count` -> `batch_size`, `disable_system_prompt`
-> `enable_system_prompt`, legacy aspect_ratio/image_resolution enums onto
CommonAspectRatio/CommonResolution. Folds the conditional common_aspect_ratio
branch into a single aspect_ratio field that prefers commonAspectRatio when
the model supports it.
2026-05-01 10:00:11 -07:00
Brandon Thomas d937a9db5f wip 2026-05-01 05:09:16 -04:00
Brandon Thomas 13896d9306 cleanup 2026-05-01 05:09:16 -04:00
Brandon Thomas 874705f691 get rid of older providers for now 2026-05-01 05:09:16 -04:00
Brandon Thomas f192e2c077 refactor, move handlers 2026-05-01 05:09:16 -04:00
Brandon Thomas 5beac6c368 artcraft provider wip 2026-05-01 05:09:16 -04:00
Brandon Thomas f2d327481b note 2026-05-01 05:09:15 -04:00
Brandon Thomas 23084a765a refactor wip 2026-05-01 05:09:15 -04:00
Tanish Manku 9060ff8099 artcraft: migrate frontend image/video calls to unified generate_* tauri commands
Add GenerateImage / GenerateVideo wrappers in @storyteller/tauri-api
that mirror the unified TauriGenerateImageRequest / TauriGenerateVideoRequest
shapes and invoke generate_image_command / generate_video_command. Migrate
PromptBoxImage (text-to-image), PromptBoxVideo (image-to-video), PageDraw
(edit + inpaint), and Angles off the per-flow Enqueue* wrappers onto the
unified ones. Old wrappers stay; Rust proxy handles routing.
2026-05-01 05:09:15 -04:00
Brandon Thomas 837a8d417a proxy to other endpoints 2026-05-01 05:09:15 -04:00
Brandon Thomas 62cb63b8dc stub "generate_image" endpoint 2026-05-01 05:09:15 -04:00
Brandon Thomas b73d0b1af6 fix compile error, remove dead code 2026-05-01 05:09:15 -04:00
Brandon Thomas afc9db10fe fix requests 2026-05-01 05:09:15 -04:00
Brandon Thomas 9945a825ed rename generate video command 2026-05-01 05:09:15 -04:00
136 changed files with 3437 additions and 1024 deletions
@@ -1,4 +1,5 @@
pub mod aspect_ratio_to_artcraft_flux_1_dev;
pub mod router_aspect_ratio_to_enums_aspect_ratio;
pub mod aspect_ratio_to_artcraft_flux_1_schnell;
pub mod aspect_ratio_to_artcraft_flux_pro_1p1;
pub mod aspect_ratio_to_artcraft_flux_pro_1p1_ultra;
@@ -0,0 +1,26 @@
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterCommonAspectRatio;
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as EnumsCommonAspectRatio;
/// Map from the router's CommonAspectRatio to the enums crate's CommonAspectRatio.
pub fn router_aspect_ratio_to_enums_aspect_ratio(ratio: RouterCommonAspectRatio) -> EnumsCommonAspectRatio {
match ratio {
RouterCommonAspectRatio::Auto => EnumsCommonAspectRatio::Auto,
RouterCommonAspectRatio::Square => EnumsCommonAspectRatio::Square,
RouterCommonAspectRatio::WideThreeByTwo => EnumsCommonAspectRatio::WideThreeByTwo,
RouterCommonAspectRatio::WideFourByThree => EnumsCommonAspectRatio::WideFourByThree,
RouterCommonAspectRatio::WideFiveByFour => EnumsCommonAspectRatio::WideFiveByFour,
RouterCommonAspectRatio::WideSixteenByNine => EnumsCommonAspectRatio::WideSixteenByNine,
RouterCommonAspectRatio::WideTwentyOneByNine => EnumsCommonAspectRatio::WideTwentyOneByNine,
RouterCommonAspectRatio::TallTwoByThree => EnumsCommonAspectRatio::TallTwoByThree,
RouterCommonAspectRatio::TallThreeByFour => EnumsCommonAspectRatio::TallThreeByFour,
RouterCommonAspectRatio::TallFourByFive => EnumsCommonAspectRatio::TallFourByFive,
RouterCommonAspectRatio::TallNineBySixteen => EnumsCommonAspectRatio::TallNineBySixteen,
RouterCommonAspectRatio::TallNineByTwentyOne => EnumsCommonAspectRatio::TallNineByTwentyOne,
RouterCommonAspectRatio::Wide => EnumsCommonAspectRatio::Wide,
RouterCommonAspectRatio::Tall => EnumsCommonAspectRatio::Tall,
RouterCommonAspectRatio::Auto2k => EnumsCommonAspectRatio::Auto2k,
RouterCommonAspectRatio::Auto3k => EnumsCommonAspectRatio::Auto3k,
RouterCommonAspectRatio::Auto4k => EnumsCommonAspectRatio::Auto4k,
RouterCommonAspectRatio::SquareHd => EnumsCommonAspectRatio::SquareHd,
}
}
@@ -1,3 +1,4 @@
pub mod aspect_ratio;
pub mod resolution;
pub mod quality;
pub mod models;
pub mod quality;
pub mod resolution;
@@ -0,0 +1,3 @@
pub mod tauri_image_model_to_enums_model;
pub mod tauri_image_model_to_generation_model;
pub mod tauri_image_model_to_router_model;
@@ -0,0 +1,33 @@
use enums::common::generation::common_image_model::CommonImageModel;
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
/// Map TauriImageModel to the omni endpoint's CommonImageModel (enums crate).
/// Returns None for models not yet supported by the omni endpoint.
pub fn tauri_image_model_to_enums_model(model: TauriImageModel) -> Option<CommonImageModel> {
match model {
TauriImageModel::Flux1Dev => Some(CommonImageModel::Flux1Dev),
TauriImageModel::Flux1Schnell => Some(CommonImageModel::Flux1Schnell),
TauriImageModel::FluxPro1 => Some(CommonImageModel::FluxPro11), // TODO: Might be a slight mismatch
TauriImageModel::FluxPro11 => Some(CommonImageModel::FluxPro11),
TauriImageModel::FluxPro11Ultra => Some(CommonImageModel::FluxPro11Ultra),
TauriImageModel::GptImage1 => Some(CommonImageModel::GptImage1),
TauriImageModel::GptImage1p5 => Some(CommonImageModel::GptImage1p5),
TauriImageModel::GptImage2 => Some(CommonImageModel::GptImage2),
TauriImageModel::NanoBanana => Some(CommonImageModel::NanoBanana),
TauriImageModel::NanoBanana2 => Some(CommonImageModel::NanoBanana2),
TauriImageModel::NanoBananaPro => Some(CommonImageModel::NanoBananaPro),
TauriImageModel::Gemini25Flash => Some(CommonImageModel::NanoBanana),
TauriImageModel::Seedream4 => Some(CommonImageModel::Seedream4),
TauriImageModel::Seedream4p5 => Some(CommonImageModel::Seedream4p5),
TauriImageModel::Seedream5Lite => Some(CommonImageModel::Seedream5Lite),
// Not accounted for yet
TauriImageModel::GrokImage => None,
TauriImageModel::Recraft3 => None,
TauriImageModel::Midjourney => None,
TauriImageModel::FluxProKontextMax => None,
TauriImageModel::QwenEdit2511Angles => None,
TauriImageModel::Flux2LoraAngles => None,
TauriImageModel::FluxDevJuggernaut => None,
}
}
@@ -0,0 +1,30 @@
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
use crate::core::events::generation_events::common::GenerationModel;
/// Map TauriImageModel to GenerationModel for frontend events.
pub fn tauri_image_model_to_generation_model(model: TauriImageModel) -> GenerationModel {
match model {
TauriImageModel::Flux1Dev => GenerationModel::Flux1Dev,
TauriImageModel::Flux1Schnell => GenerationModel::Flux1Schnell,
TauriImageModel::FluxPro11 => GenerationModel::FluxPro11,
TauriImageModel::FluxPro11Ultra => GenerationModel::FluxPro11Ultra,
TauriImageModel::GrokImage => GenerationModel::GrokImage,
TauriImageModel::Recraft3 => GenerationModel::Flux1Dev, // Fallback
TauriImageModel::GptImage1 => GenerationModel::GptImage1,
TauriImageModel::GptImage1p5 => GenerationModel::GptImage1p5,
TauriImageModel::GptImage2 => GenerationModel::GptImage2,
TauriImageModel::Gemini25Flash => GenerationModel::NanoBanana,
TauriImageModel::NanoBanana => GenerationModel::NanoBanana,
TauriImageModel::NanoBanana2 => GenerationModel::NanoBanana2,
TauriImageModel::NanoBananaPro => GenerationModel::NanoBananaPro,
TauriImageModel::Seedream4 => GenerationModel::Seedream4,
TauriImageModel::Seedream4p5 => GenerationModel::Seedream4p5,
TauriImageModel::Seedream5Lite => GenerationModel::Seedream5Lite,
TauriImageModel::Midjourney => GenerationModel::Flux1Dev, // Fallback
TauriImageModel::FluxProKontextMax => GenerationModel::FluxProKontextMax,
TauriImageModel::QwenEdit2511Angles => GenerationModel::QwenEdit2511Angles,
TauriImageModel::Flux2LoraAngles => GenerationModel::Flux2LoraAngles,
TauriImageModel::FluxDevJuggernaut => GenerationModel::FluxDevJuggernaut,
TauriImageModel::FluxPro1 => GenerationModel::FluxPro1,
}
}
@@ -0,0 +1,33 @@
use artcraft_router::api::common_image_model::CommonImageModel;
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
/// Map TauriImageModel to the artcraft_router's CommonImageModel.
/// Returns None for models not supported by the router (Grok, Midjourney, etc.).
pub fn tauri_image_model_to_router_model(model: TauriImageModel) -> Option<CommonImageModel> {
match model {
TauriImageModel::Flux1Dev => Some(CommonImageModel::Flux1Dev), // Text-to-Image
TauriImageModel::Flux1Schnell => Some(CommonImageModel::Flux1Schnell), // Text-to-Image
TauriImageModel::FluxPro1 => Some(CommonImageModel::FluxPro11), // TODO: Might be a slight mismatch
TauriImageModel::FluxPro11 => Some(CommonImageModel::FluxPro11), // Text-to-Image
TauriImageModel::FluxPro11Ultra => Some(CommonImageModel::FluxPro11Ultra), // Text-to-Image
TauriImageModel::GptImage1 => Some(CommonImageModel::GptImage1), // Text-to-Image
TauriImageModel::GptImage1p5 => Some(CommonImageModel::GptImage1p5), // Text-to-Image
TauriImageModel::GptImage2 => Some(CommonImageModel::GptImage2), // Text-to-Image
TauriImageModel::NanoBanana => Some(CommonImageModel::NanoBanana), // Text-to-Image
TauriImageModel::NanoBanana2 => Some(CommonImageModel::NanoBanana2), // Text-to-Image
TauriImageModel::NanoBananaPro => Some(CommonImageModel::NanoBananaPro), // Text-to-Image
TauriImageModel::Gemini25Flash => Some(CommonImageModel::NanoBanana), // Text-to-Image
TauriImageModel::Seedream4 => Some(CommonImageModel::Seedream4), // Text-to-Image
TauriImageModel::Seedream4p5 => Some(CommonImageModel::Seedream4p5), // Text-to-Image
TauriImageModel::Seedream5Lite => Some(CommonImageModel::Seedream5Lite), // Text-to-Image
TauriImageModel::QwenEdit2511Angles => Some(CommonImageModel::QwenEdit2511Angles),
TauriImageModel::Flux2LoraAngles => Some(CommonImageModel::Flux2LoraAngles),
// Not accounted for yet
TauriImageModel::GrokImage => None,
TauriImageModel::Recraft3 => None,
TauriImageModel::Midjourney => None,
TauriImageModel::FluxProKontextMax => None,
TauriImageModel::FluxDevJuggernaut => None,
}
}
@@ -0,0 +1 @@
pub mod image;
@@ -1 +1,2 @@
pub mod resolution_to_artcraft_nano_banana_pro;
pub mod resolution_to_artcraft_nano_banana_pro;
pub mod router_resolution_to_enums_resolution;
@@ -0,0 +1,16 @@
use artcraft_router::api::common_resolution::CommonResolution as RouterCommonResolution;
use enums::common::generation::common_resolution::CommonResolution as EnumsCommonResolution;
/// Map from the router's CommonResolution to the enums crate's CommonResolution.
pub fn router_resolution_to_enums_resolution(res: RouterCommonResolution) -> EnumsCommonResolution {
match res {
RouterCommonResolution::OneK => EnumsCommonResolution::OneK,
RouterCommonResolution::TwoK => EnumsCommonResolution::TwoK,
RouterCommonResolution::ThreeK => EnumsCommonResolution::ThreeK,
RouterCommonResolution::FourK => EnumsCommonResolution::FourK,
RouterCommonResolution::HalfK => EnumsCommonResolution::HalfK,
RouterCommonResolution::FourEightyP => EnumsCommonResolution::FourEightyP,
RouterCommonResolution::SevenTwentyP => EnumsCommonResolution::SevenTwentyP,
RouterCommonResolution::TenEightyP => EnumsCommonResolution::TenEightyP,
}
}
@@ -1,4 +1,4 @@
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageSuccessResponse;
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::EnqueueEditImageSuccessResponse;
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
@@ -1,8 +1,8 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_flux_kontext_edit::handle_artcraft_flux_kontext_edit;
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_gpt_image_1_edit::handle_artcraft_gpt_image_1_edit;
use crate::core::commands::enqueue::image_edit::artcraft::handle_image_edit_artcraft_via_router::handle_image_edit_artcraft_via_router;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EnqueueEditImageCommand, ImageEditModel};
use crate::core::commands::deprecated::image_edit::artcraft::handle_artcraft_flux_kontext_edit::handle_artcraft_flux_kontext_edit;
use crate::core::commands::deprecated::image_edit::artcraft::handle_artcraft_gpt_image_1_edit::handle_artcraft_gpt_image_1_edit;
use crate::core::commands::deprecated::image_edit::artcraft::handle_image_edit_artcraft_via_router::handle_image_edit_artcraft_via_router;
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EnqueueEditImageCommand, ImageEditModel};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
@@ -1,6 +1,6 @@
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio as DesktopCommonAspectRatio;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{
EditImageResolution, EditImageSize, EnqueueEditImageCommand,
};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
@@ -2,9 +2,9 @@ use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRa
use crate::core::api_adapters::quality::common_quality::CommonQuality;
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::artcraft::handle_image_edit_artcraft::handle_image_edit_artcraft;
use crate::core::commands::enqueue::image_edit::image_edit_models::image_edit_model_to_model_type;
use crate::core::commands::enqueue::image_edit::sora::handle_image_edit_sora::handle_image_edit_sora;
use crate::core::commands::deprecated::image_edit::artcraft::handle_image_edit_artcraft::handle_image_edit_artcraft;
use crate::core::commands::deprecated::image_edit::image_edit_models::image_edit_model_to_model_type;
use crate::core::commands::deprecated::image_edit::sora::handle_image_edit_sora::handle_image_edit_sora;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
@@ -1,5 +1,5 @@
use enums::common::generation::common_model_type::CommonModelType;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::ImageEditModel;
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::ImageEditModel;
pub fn image_edit_model_to_model_type(model: ImageEditModel) -> CommonModelType {
match model {
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EnqueueEditImageCommand, ImageEditModel};
use crate::core::commands::enqueue::image_edit::image_edit_models::image_edit_model_to_model_type;
use crate::core::commands::enqueue::image_edit::sora::handle_sora_gpt_image_1_edit::handle_sora_gpt_image_1_edit;
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EnqueueEditImageCommand, ImageEditModel};
use crate::core::commands::deprecated::image_edit::image_edit_models::image_edit_model_to_model_type;
use crate::core::commands::deprecated::image_edit::sora::handle_sora_gpt_image_1_edit::handle_sora_gpt_image_1_edit;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageSize, EnqueueEditImageCommand, EnqueueEditImageErrorType};
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageSize, EnqueueEditImageCommand, EnqueueEditImageErrorType};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
@@ -1,9 +1,9 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_inpaint::artcraft::handle_artcraft_flux_dev_juggernaut_inpaint::handle_artcraft_flux_dev_juggernaut_inpaint;
use crate::core::commands::enqueue::image_inpaint::artcraft::handle_artcraft_flux_pro_1_inpaint::handle_artcraft_flux_pro_1_inpaint;
use crate::core::commands::enqueue::image_inpaint::artcraft::handle_artcraft_flux_pro_kontext_inpaint::handle_artcraft_flux_pro_kontext_inpaint;
use crate::core::commands::enqueue::image_inpaint::artcraft::handle_artcraft_nano_banana_inpaint::handle_artcraft_nano_banana_inpaint;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::{EnqueueInpaintImageCommand, ImageInpaintModel};
use crate::core::commands::deprecated::image_inpaint::artcraft::handle_artcraft_flux_dev_juggernaut_inpaint::handle_artcraft_flux_dev_juggernaut_inpaint;
use crate::core::commands::deprecated::image_inpaint::artcraft::handle_artcraft_flux_pro_1_inpaint::handle_artcraft_flux_pro_1_inpaint;
use crate::core::commands::deprecated::image_inpaint::artcraft::handle_artcraft_flux_pro_kontext_inpaint::handle_artcraft_flux_pro_kontext_inpaint;
use crate::core::commands::deprecated::image_inpaint::artcraft::handle_artcraft_nano_banana_inpaint::handle_artcraft_nano_banana_inpaint;
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::{EnqueueInpaintImageCommand, ImageInpaintModel};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
@@ -1,9 +1,9 @@
use crate::core::api_adapters::quality::common_quality::CommonQuality;
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::image_edit_models::image_edit_model_to_model_type;
use crate::core::commands::enqueue::image_inpaint::artcraft::handle_inpaint_artcraft::handle_inpaint_artcraft;
use crate::core::commands::enqueue::image_inpaint::inpaint_models::image_inpaint_model_to_model_type;
use crate::core::commands::deprecated::image_edit::image_edit_models::image_edit_model_to_model_type;
use crate::core::commands::deprecated::image_inpaint::artcraft::handle_inpaint_artcraft::handle_inpaint_artcraft;
use crate::core::commands::deprecated::image_inpaint::inpaint_models::image_inpaint_model_to_model_type;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
@@ -1,4 +1,4 @@
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::ImageInpaintModel;
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::ImageInpaintModel;
use enums::common::generation::common_model_type::CommonModelType;
pub fn image_inpaint_model_to_model_type(model: ImageInpaintModel) -> CommonModelType {
@@ -0,0 +1,3 @@
pub mod image_edit;
pub mod image_inpaint;
pub mod text_to_image;
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
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;
@@ -1,9 +1,9 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_gpt_image_1_text_to_image::handle_artcraft_gpt_image_1_text_to_image;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_text_to_image_artcraft_via_router::handle_text_to_image_artcraft_via_router;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageModel};
use crate::core::commands::enqueue::text_to_image::text_to_image_models::text_to_image_model_to_model_type;
use crate::core::commands::deprecated::text_to_image::artcraft::handle_artcraft_gpt_image_1_text_to_image::handle_artcraft_gpt_image_1_text_to_image;
use crate::core::commands::deprecated::text_to_image::artcraft::handle_text_to_image_artcraft_via_router::handle_text_to_image_artcraft_via_router;
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageModel};
use crate::core::commands::deprecated::text_to_image::text_to_image_models::text_to_image_model_to_model_type;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
@@ -2,7 +2,7 @@ use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRa
use crate::core::api_adapters::resolution::common_resolution::CommonResolution as CommonResolution2;
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{
EnqueueTextToImageRequest, TextToImageResolution, TextToImageSize,
};
use crate::core::events::generation_events::common::GenerationModel;
@@ -3,13 +3,13 @@ use crate::core::api_adapters::quality::common_quality::CommonQuality;
use crate::core::api_adapters::resolution::common_resolution::CommonResolution;
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::image_edit::image_edit_models::image_edit_model_to_model_type;
use crate::core::commands::deprecated::image_edit::image_edit_models::image_edit_model_to_model_type;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_text_to_image_artcraft::handle_text_to_image_artcraft;
use crate::core::commands::enqueue::text_to_image::grok::handle_grok::handle_grok;
use crate::core::commands::enqueue::text_to_image::midjourney::handle_midjourney::handle_midjourney;
use crate::core::commands::enqueue::text_to_image::sora::handle_text_to_image_sora::handle_text_to_image_sora;
use crate::core::commands::enqueue::text_to_image::text_to_image_models::text_to_image_model_to_model_type;
use crate::core::commands::deprecated::text_to_image::artcraft::handle_text_to_image_artcraft::handle_text_to_image_artcraft;
use crate::core::commands::deprecated::text_to_image::grok::handle_grok::handle_grok;
use crate::core::commands::deprecated::text_to_image::midjourney::handle_midjourney::handle_midjourney;
use crate::core::commands::deprecated::text_to_image::sora::handle_text_to_image_sora::handle_text_to_image_sora;
use crate::core::commands::deprecated::text_to_image::text_to_image_models::text_to_image_model_to_model_type;
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;
@@ -1,7 +1,7 @@
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image::aspect_ratio_to_grok_image;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::canvas_background_removal_complete_event::CanvasBackgroundRemovalCompleteEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -2,7 +2,7 @@ use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_sora_native_gpt_image_1::aspect_ratio_to_sora_native_gpt_image_1;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
@@ -0,0 +1,21 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
use crate::core::commands::deprecated::text_to_image::sora::handle_gpt_image_1_sora_text_to_image::handle_gpt_image_1_sora_text_to_image;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use tauri::AppHandle;
pub async fn handle_text_to_image_sora(
request: &EnqueueTextToImageRequest,
app: &AppHandle,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
handle_gpt_image_1_sora_text_to_image(
request,
app,
sora_creds_manager,
sora_task_queue,
).await
}
@@ -1,4 +1,4 @@
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::TextToImageModel;
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::TextToImageModel;
use enums::common::generation::common_model_type::CommonModelType;
pub fn text_to_image_model_to_model_type(model: TextToImageModel) -> CommonModelType {
@@ -1,3 +1,3 @@
mod maybe_notify_frontend_of_billing_errors;
mod maybe_notify_frontend_of_grok_errors;
pub mod maybe_notify_frontend_of_billing_errors;
pub mod maybe_notify_frontend_of_grok_errors;
pub mod notify_frontend_of_errors;
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::image_to_gaussian::enqueue_image_to_gaussian_command::EnqueueImageToGaussianRequest;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
@@ -1,81 +0,0 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_router::api::audio_list_ref::AudioListRef;
use artcraft_router::api::character_list_ref::CharacterListRef;
use artcraft_router::api::common_video_model::CommonVideoModel;
use artcraft_router::api::image_list_ref::ImageListRef;
use artcraft_router::api::image_ref::ImageRef;
use artcraft_router::api::provider::Provider;
use artcraft_router::api::video_list_ref::VideoListRef;
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
use artcraft_router::client::router_client::RouterClient;
use artcraft_router::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use log::{error, info};
pub(super) async fn handle_artcraft_seedance_2p0(
request: &EnqueueImageToVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let client = RouterClient::Artcraft(RouterArtcraftClient::new(
app_env_configs.storyteller_host.clone(),
creds.clone(),
));
let start_frame = request.image_media_token.clone().map(ImageRef::MediaFileToken);
let end_frame = request.end_frame_image_media_token.clone().map(ImageRef::MediaFileToken);
let reference_images = request.reference_image_media_tokens.clone().map(ImageListRef::MediaFileTokens);
let router_request = GenerateVideoRequestBuilder {
model: CommonVideoModel::Seedance2p0,
provider: Provider::Artcraft,
prompt: request.prompt.clone(),
negative_prompt: None,
start_frame,
end_frame,
reference_images,
reference_videos: request.reference_video_media_tokens.clone().map(VideoListRef::MediaFileTokens),
reference_audio: request.reference_audio_media_tokens.clone().map(AudioListRef::MediaFileTokens),
reference_character_tokens: request.reference_character_tokens.clone().map(CharacterListRef::CharacterTokens),
resolution: None,
aspect_ratio: request.aspect_ratio,
duration_seconds: request.duration_seconds,
video_batch_count: request.video_batch_count,
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
idempotency_token: None,
};
let plan = router_request.build()?;
info!("Video Generation Plan: {:?}", plan);
let response = match plan.generate_video(&client).await {
Ok(resp) => {
info!("Successfully enqueued.");
resp
}
Err(err) => {
error!("Failed to enqueue: {:?}", err);
return Err(GenerateError::from(err));
}
};
let job_id = response.get_artcraft_payload()
.map(|p| p.inference_job_token.to_string())
.ok_or(GenerateError::ResponseHadNoJobTokens)?;
Ok(TaskEnqueueSuccess {
task_type: TaskType::VideoGeneration,
model: Some(GenerationModel::Seedance2p0),
provider: GenerationProvider::Artcraft,
provider_job_id: Some(job_id),
})
}
@@ -1,66 +0,0 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_1p6_pro::handle_artcraft_kling_1p6_pro;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_2p1_master::handle_artcraft_kling_2p1_master;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_2p1_pro::handle_artcraft_kling_2p1_pro;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_2p5_turbo_pro::handle_artcraft_kling_2p5_turbo_pro;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_2p6_pro::handle_artcraft_kling_2p6_pro;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_video_via_router::handle_artcraft_video_via_router;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_seedance_1_lite::handle_artcraft_seedance_1_lite;
use crate::core::events::generation_events::common::GenerationModel;
use artcraft_router::api::common_video_model::CommonVideoModel;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_seedance_2p0::handle_artcraft_seedance_2p0;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_sora2::handle_artcraft_sora2;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_sora2_pro::handle_artcraft_sora2_pro;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_veo2::handle_artcraft_veo2;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_veo3::handle_artcraft_veo3;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_veo3_fast::handle_artcraft_veo3_fast;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_veo3p1::handle_artcraft_veo3p1;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_veo3p1_fast::handle_artcraft_veo3p1_fast;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, VideoModel};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use tauri::AppHandle;
pub async fn handle_video_artcraft(
request: &EnqueueImageToVideoRequest,
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = get_storyteller_creds_or_error(app, storyteller_creds_manager)?;
match request.model {
None => {
Err(GenerateError::no_model_specified())
},
// Handled via router
Some(VideoModel::Kling3p0Pro) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Kling3p0Pro, GenerationModel::Kling3p0Pro).await,
Some(VideoModel::Kling3p0Standard) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Kling3p0Standard, GenerationModel::Kling3p0Standard).await,
Some(VideoModel::Seedance1p5Pro) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Seedance1p5Pro, GenerationModel::Seedance1p5Pro).await,
Some(VideoModel::Seedance2p0) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Seedance2p0, GenerationModel::Seedance2p0).await,
Some(VideoModel::Seedance2p0Fast) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Seedance2p0Fast, GenerationModel::Seedance2p0Fast).await,
Some(VideoModel::HappyHorse1p0) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::HappyHorse1p0, GenerationModel::HappyHorse1p0).await,
// Legacy endpoints
Some(VideoModel::Kling16Pro) => handle_artcraft_kling_1p6_pro(request, app_env_configs, &creds).await,
Some(VideoModel::Kling21Pro) => handle_artcraft_kling_2p1_pro(request, app_env_configs, &creds).await,
Some(VideoModel::Kling21Master) => handle_artcraft_kling_2p1_master(request, app_env_configs, &creds).await,
Some(VideoModel::Kling2p5TurboPro) => handle_artcraft_kling_2p5_turbo_pro(request, app_env_configs, &creds).await,
Some(VideoModel::Kling2p6Pro) => handle_artcraft_kling_2p6_pro(request, app_env_configs, &creds).await,
Some(VideoModel::Seedance10Lite) => handle_artcraft_seedance_1_lite(request, app_env_configs, &creds).await,
Some(VideoModel::Sora2) => handle_artcraft_sora2(request, app_env_configs, &creds).await,
Some(VideoModel::Sora2Pro) => handle_artcraft_sora2_pro(request, app_env_configs, &creds).await,
Some(VideoModel::Veo2) => handle_artcraft_veo2(request, app_env_configs, &creds).await,
Some(VideoModel::Veo3) => handle_artcraft_veo3(request, app_env_configs, &creds).await,
Some(VideoModel::Veo3Fast) => handle_artcraft_veo3_fast(request, app_env_configs, &creds).await,
Some(VideoModel::Veo3p1) => handle_artcraft_veo3p1(request, app_env_configs, &creds).await,
Some(VideoModel::Veo3p1Fast) => handle_artcraft_veo3p1_fast(request, app_env_configs, &creds).await,
Some(_) => {
Err(GenerateError::AnyhowError(
anyhow!("wrong logic: another branch should handle this: {:?}", request.model)))
},
}
}
@@ -1,395 +0,0 @@
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_video::handle_video_artcraft;
use crate::core::commands::enqueue::image_to_video::grok::handle_grok_video::handle_grok_video;
use crate::core::commands::enqueue::image_to_video::sora2::handle_sora_sora2::handle_sora_sora2;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
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::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
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::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::state::task_database::TaskDatabase;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
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 artcraft_router::api::common_aspect_ratio::CommonAspectRatio;
use artcraft_router::api::common_resolution::CommonResolution;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
use log::{error, info, warn};
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
/// 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 = "grok_video")]
GrokVideo,
#[serde(rename = "kling_1.6_pro")]
Kling16Pro,
#[serde(rename = "kling_2.1_pro")]
Kling21Pro,
#[serde(rename = "kling_2.1_master")]
Kling21Master,
#[serde(rename = "kling_2p5_turbo_pro")]
Kling2p5TurboPro,
#[serde(rename = "kling_2p6_pro")]
Kling2p6Pro,
#[serde(rename = "kling_3p0_standard")]
Kling3p0Standard,
#[serde(rename = "kling_3p0_pro")]
Kling3p0Pro,
#[serde(rename = "happy_horse_1p0")]
HappyHorse1p0,
#[serde(rename = "seedance_1.0_lite")]
Seedance10Lite,
#[serde(rename = "seedance_1p5_pro")]
Seedance1p5Pro,
#[serde(rename = "seedance_2p0")]
Seedance2p0,
#[serde(rename = "seedance_2p0_fast")]
Seedance2p0Fast,
#[serde(rename = "sora_2")]
Sora2,
#[serde(rename = "sora_2_pro")]
Sora2Pro,
#[serde(rename = "veo_2")]
Veo2,
#[serde(rename = "veo_3")]
Veo3,
#[serde(rename = "veo_3_fast")]
Veo3Fast,
#[serde(rename = "veo_3p1")]
Veo3p1,
#[serde(rename = "veo_3p1_fast")]
Veo3p1Fast,
}
#[derive(Deserialize, Debug)]
pub struct EnqueueImageToVideoRequest {
/// The provider to use (defaults to Artcraft/Storyteller).
/// Not all (provider, model) combinations are valid.
pub provider: Option<GenerationProvider>,
/// REQUIRED.
/// The model to use.
pub model: Option<VideoModel>,
/// Currently REQUIRED by some downstream models.
/// Image media file; the starting frame of the video.
/// TODO: In the future we may support base64 images, URLs, or file paths here.
pub image_media_token: Option<MediaFileToken>,
/// Optional.
/// Image media file; the image to remove the background from.
/// TODO: In the future we may support base64 images, URLs, or file paths here.
pub end_frame_image_media_token: Option<MediaFileToken>,
/// OPTIONAL.
/// Reference images for the video generation (e.g. character consistency, props, etc.)
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
/// OPTIONAL.
/// Reference videos for the video generation (e.g. video edits, etc.)
pub reference_video_media_tokens: Option<Vec<MediaFileToken>>,
/// OPTIONAL.
/// Reference audio for the video generation (e.g. voices, lip sync, music, etc.)
pub reference_audio_media_tokens: Option<Vec<MediaFileToken>>,
/// Optional character tokens to reference in the prompt.
/// Characters are referenced in prompts as @CharacterName.
pub reference_character_tokens: Option<Vec<CharacterToken>>,
/// Optional.
/// Text prompt used to direct the video.
pub prompt: Option<String>,
/// Optional.
/// Generate the video with audio, if there is an option.
/// Typically, this costs money when presented as an option, so we default to off.
pub generate_audio: Option<bool>,
/// OPTIONAL.
/// Aspect ratio for the generated video.
pub aspect_ratio: Option<CommonAspectRatio>,
/// OPTIONAL.
/// Resolution for the generated video.
pub resolution: Option<CommonResolution>,
/// OPTIONAL.
/// Duration of the generated video in seconds.
pub duration_seconds: Option<u16>,
/// OPTIONAL.
/// Number of videos to generate in one batch.
pub video_batch_count: Option<u16>,
/// OPTIONAL.
/// Only for Sora2 model currently.
pub sora_orientation: Option<SoraOrientation>,
/// OPTIONAL.
/// Only for Grok model currently
pub grok_aspect_ratio: Option<GrokAspectRatio>,
/// OPTIONAL.
/// Name of the frontend caller.
/// We'll use this to selectively trigger events.
pub frontend_caller: Option<TauriCommandCaller>,
/// OPTIONAL.
/// A frontend-defined identifier that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_id: Option<String>,
/// OPTIONAL.
/// A frontend-defined payload that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_payload: Option<String>,
}
// TODO: Not sure how to handle so many different types of video (model) x (services).
#[derive(Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum SoraOrientation {
Portrait,
Landscape,
}
// TODO: Not sure how to handle so many different types of video (model) x (services).
#[derive(Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum GrokAspectRatio {
Portrait,
Landscape,
Square
}
#[derive(Serialize)]
pub struct EnqueueImageToVideoSuccessResponse {
}
impl SerializeMarker for EnqueueImageToVideoSuccessResponse {}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueImageToVideoErrorType {
/// Caller didn't specify a model
ModelNotSpecified,
/// No model available for video generation
NoProviderAvailable,
/// Generic server error
ServerError,
/// No Fal API key available
NeedsFalApiKey,
/// Fal had an API error
FalError,
/// Needs to be logged into Artcraft
NeedsStorytellerCredentials,
}
#[tauri::command]
pub async fn enqueue_image_to_video_command(
request: EnqueueImageToVideoRequest,
app: AppHandle,
app_env_configs: State<'_, AppEnvConfigs>,
app_data_root: State<'_, AppDataRoot>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
grok_creds_manager: State<'_, GrokCredentialManager>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
sora_creds_manager: State<'_, SoraCredentialManager>,
) -> Response<EnqueueImageToVideoSuccessResponse, EnqueueImageToVideoErrorType, ()> {
info!("enqueue_image_to_video_command called, request: {:?}", request);
let result = handle_request(
request,
&app,
&app_env_configs,
&app_data_root,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&grok_creds_manager,
&sora_creds_manager,
&storyteller_creds_manager,
).await;
match result {
Err(err) => {
error!("error: {:?}", err);
notify_frontend_of_errors(&app, &err).await;
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueImageToVideoErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
match err {
GenerateError::BadInput(BadInputReason::NoModelSpecified) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageToVideoErrorType::ModelNotSpecified;
error_message = "No model specified for video generation";
}
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueImageToVideoErrorType::NoProviderAvailable;
error_message = "No configured provider available for video generation";
}
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey) => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageToVideoErrorType::NeedsFalApiKey;
error_message = "You need to set a FAL api key";
},
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials) => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageToVideoErrorType::NeedsStorytellerCredentials;
error_message = "You need to be logged into Artcraft.";
}
_ => {}, // Fall-through
}
Err(CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
})
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
action: event.to_frontend_event_action(),
service: event.to_frontend_event_service(),
model: event.model,
};
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
CreditsBalanceChangedEvent{}.send_infallible(&app);
Ok(EnqueueImageToVideoSuccessResponse {}.into())
}
}
}
pub async fn handle_request(
request: EnqueueImageToVideoRequest,
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
grok_creds_manager: &GrokCredentialManager,
sora_creds_manager: &SoraCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let model = match request.model {
Some(model) => model,
None => {
return Err(GenerateError::no_model_specified())
}
};
let provider = match (model, request.provider) {
(VideoModel::GrokVideo, _) => GenerationProvider::Grok,
_ => request.provider.unwrap_or(GenerationProvider::Artcraft),
};
info!("generate video with {:?} via provider {:?}", &model, &provider);
let result = match provider {
GenerationProvider::Grok => {
handle_grok_video(
&request,
app,
app_data_root,
app_env_configs,
grok_creds_manager,
).await
}
GenerationProvider::Sora => {
handle_sora_sora2(
&request,
app,
app_data_root,
app_env_configs,
sora_creds_manager,
).await
}
_ => {
handle_video_artcraft(
&request,
app,
app_env_configs,
storyteller_creds_manager,
).await
}
};
let success_event = match result {
Err(err) => return Err(err),
Ok(event) => event,
};
let result = success_event
.insert_into_task_database_with_frontend_payload(
task_database,
request.frontend_caller,
request.frontend_subscriber_id.as_deref(),
request.frontend_subscriber_payload.as_deref()
)
.await;
if let Err(err) = result {
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_video_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::VideoPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -1,4 +0,0 @@
pub mod enqueue_image_to_video_command;
pub mod sora2;
mod grok;
mod artcraft;
@@ -1,10 +1,6 @@
pub mod common;
pub mod generate_error;
pub mod image_bg_removal;
pub mod image_edit;
pub mod image_inpaint;
pub mod image_to_gaussian;
pub mod image_to_object;
pub mod image_to_video;
pub mod task_enqueue_success;
pub mod text_to_image;
pub mod image_to_gaussian;
pub mod task_enqueue_success;
@@ -0,0 +1,167 @@
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::generate::generate_image::providers::artcraft::handle_artcraft;
use crate::core::commands::generate::generate_image::tauri_generate_image_request::{
TauriGenerateImageErrorType, TauriGenerateImageRequest, TauriGenerateImageResponse,
};
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
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::task_database::TaskDatabase;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use enums::common::generation_provider::GenerationProvider;
use log::{error, info};
use tauri::{AppHandle, State};
#[tauri::command]
pub async fn generate_image_command(
request: TauriGenerateImageRequest,
app: AppHandle,
app_env_configs: State<'_, AppEnvConfigs>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
) -> Response<TauriGenerateImageResponse, TauriGenerateImageErrorType, ()> {
info!("generate_image_command called, request: {:?}", request);
// Only Artcraft provider is supported via this command.
// Other providers (Grok, Sora, Midjourney) should use the legacy commands.
let is_artcraft_provider = matches!(
request.provider,
None | Some(GenerationProvider::Artcraft)
);
if !is_artcraft_provider {
return Err(CommandErrorResponseWrapper {
status: CommandErrorStatus::BadRequest,
error_message: Some("Only Artcraft is supported.".to_string()),
error_type: Some(TauriGenerateImageErrorType::NoProviderAvailable),
error_details: None,
});
}
let result = handle_artcraft(
&request,
&app_env_configs,
&storyteller_creds_manager,
).await;
match result {
Ok(success) => handle_success_behavior(&app, &task_database, &request, success).await,
Err(err) => handle_error_behavior(&app, err).await,
}
}
// ── Result mapping ──
async fn handle_success_behavior(
app: &AppHandle,
task_database: &TaskDatabase,
request: &TauriGenerateImageRequest,
success: TaskEnqueueSuccess,
) -> Response<TauriGenerateImageResponse, TauriGenerateImageErrorType, ()> {
// Insert into task database
let db_result = success
.insert_into_task_database_with_frontend_payload(
&task_database,
request.frontend_caller,
request.frontend_subscriber_id.as_deref(),
request.frontend_subscriber_payload.as_deref(),
)
.await;
if let Err(err) = db_result {
error!("Failed to create task in database: {:?}", err);
}
/*
let usage_type = if is_image_to_image {
ArtcraftUsageType::ImageToResult
} else {
ArtcraftUsageType::TextToResult
};
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, usage_type, ArtcraftUsagePage::ImagePage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
*/
let event = GenerationEnqueueSuccessEvent {
action: success.to_frontend_event_action(),
service: success.to_frontend_event_service(),
model: success.model,
};
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err);
}
CreditsBalanceChangedEvent{}.send_infallible(&app);
Ok(TauriGenerateImageResponse {}.into())
}
async fn handle_error_behavior(
app: &AppHandle,
err: GenerateError,
) -> Response<TauriGenerateImageResponse, TauriGenerateImageErrorType, ()> {
error!("generate_image_command error: {:?}", err);
notify_frontend_of_errors(&app, &err).await;
let error_type = match &err {
GenerateError::BadInput(_) => TauriGenerateImageErrorType::BadInput,
GenerateError::MissingCredentials(_) => TauriGenerateImageErrorType::NeedsStorytellerCredentials,
GenerateError::NoProviderAvailable => TauriGenerateImageErrorType::NoProviderAvailable,
GenerateError::BillingIssue(_) => TauriGenerateImageErrorType::BillingIssue,
_ => TauriGenerateImageErrorType::ServerError,
};
// TODO:
/*
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueTextToImageErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
match err {
GenerateError::BadInput(BadInputReason::NoModelSpecified) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueTextToImageErrorType::ModelNotSpecified;
error_message = "No model specified for image generation";
}
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueTextToImageErrorType::NoProviderAvailable;
error_message = "No configured provider available for image generation";
}
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey) => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueTextToImageErrorType::NeedsFalApiKey;
error_message = "You need to set a FAL api key";
},
_ => {}, // Fall-through
}
Err(CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
})
*/
Err(CommandErrorResponseWrapper {
status: CommandErrorStatus::ServerError,
error_message: Some(format!("{:?}", err)),
error_type: Some(error_type),
error_details: None,
})
}
@@ -0,0 +1,5 @@
pub mod generate_image_command;
pub mod providers;
pub mod tauri_generate_image_request;
pub mod tauri_image_model;
pub mod utils;
@@ -0,0 +1,65 @@
use log::info;
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::generate::generate_image::providers::artcraft::handle_artcraft_via_legacy::handle_artcraft_via_legacy;
use crate::core::commands::generate::generate_image::providers::artcraft::handle_artcraft_via_omni_endpoint::handle_artcraft_via_omni_endpoint;
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::parse_semantic_media_files;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
/// Dispatch an image generation request to Artcraft.
pub async fn handle_artcraft(
request: &TauriGenerateImageRequest,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
// Resolve credentials.
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials)),
};
// Resolve semantic media files (upload raw bytes if provided).
let semantic_media_files = parse_semantic_media_files(
request,
&creds,
&app_env_configs.storyteller_host,
).await?;
let use_legacy = request.model
.is_some_and(|m| is_legacy_only_model(m));
if use_legacy {
info!("Model {:?} is legacy-only, routing to artcraft_router path.", request.model);
handle_artcraft_via_legacy(
request,
&semantic_media_files,
&creds,
app_env_configs,
).await
} else {
handle_artcraft_via_omni_endpoint(
request,
&semantic_media_files,
&creds,
app_env_configs,
).await
}
}
fn is_legacy_only_model(model: TauriImageModel) -> bool {
matches!(
model,
TauriImageModel::GrokImage
| TauriImageModel::Recraft3
| TauriImageModel::Midjourney
| TauriImageModel::FluxProKontextMax
| TauriImageModel::QwenEdit2511Angles
| TauriImageModel::Flux2LoraAngles
| TauriImageModel::FluxDevJuggernaut
)
}
@@ -0,0 +1,49 @@
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::generate::generate_image::providers::artcraft::legacy::artcraft_flux_2_lora_angles::handle_flux_2_lora_angles;
use crate::core::commands::generate::generate_image::providers::artcraft::legacy::artcraft_flux_dev_juggernaut_inpaint::handle_flux_dev_juggernaut_inpaint;
use crate::core::commands::generate::generate_image::providers::artcraft::legacy::artcraft_flux_pro_kontext_edit::handle_flux_pro_kontext_edit;
use crate::core::commands::generate::generate_image::providers::artcraft::legacy::artcraft_qwen_edit_2511_angles::handle_qwen_edit_2511_angles;
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
/// Handle image generation via legacy model-specific paths.
///
/// Dispatches to dedicated handlers for each supported legacy model.
pub async fn handle_artcraft_via_legacy(
request: &TauriGenerateImageRequest,
semantic_media_files: &SemanticMediaFiles,
creds: &StorytellerCredentialSet,
app_env_configs: &AppEnvConfigs,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let model = request.model.ok_or(GenerateError::no_model_specified())?;
match model {
TauriImageModel::FluxDevJuggernaut => {
handle_flux_dev_juggernaut_inpaint(request, semantic_media_files, creds, app_env_configs).await
}
TauriImageModel::FluxProKontextMax => {
handle_flux_pro_kontext_edit(request, semantic_media_files, creds, app_env_configs).await
}
TauriImageModel::QwenEdit2511Angles => {
handle_qwen_edit_2511_angles(request, semantic_media_files, creds, app_env_configs).await
}
TauriImageModel::Flux2LoraAngles => {
handle_flux_2_lora_angles(request, semantic_media_files, creds, app_env_configs).await
}
TauriImageModel::Midjourney | TauriImageModel::Recraft3 => {
Err(GenerateError::NotYetImplemented(
format!("Model {:?} is not supported by provider Artcraft", model),
))
}
other => {
Err(GenerateError::NotYetImplemented(
format!("Model {:?} was incorrectly handled", other),
))
}
}
}
@@ -0,0 +1,98 @@
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
use artcraft_client::endpoints::omni_gen::generate::image::omni_gen_image::omni_gen_image_generate;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use log::{error, info};
use tokens::tokens::media_files::MediaFileToken;
use uuid_utils::uuid::generate_random_uuid;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::api_adapters::models::image::tauri_image_model_to_enums_model::tauri_image_model_to_enums_model;
use crate::core::api_adapters::models::image::tauri_image_model_to_generation_model::tauri_image_model_to_generation_model;
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
pub async fn handle_artcraft_via_omni_endpoint(
request: &TauriGenerateImageRequest,
semantic_media_files: &SemanticMediaFiles,
creds: &StorytellerCredentialSet,
app_env_configs: &AppEnvConfigs,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let tauri_model = request.model.ok_or(GenerateError::no_model_specified())?;
let omni_api_model = tauri_image_model_to_enums_model(tauri_model)
.ok_or(GenerateError::NotYetImplemented(
format!("Model {:?} is not supported via the omni endpoint", tauri_model),
))?;
let uuid_idempotency_token = generate_random_uuid();
let image_media_tokens = get_image_media_tokens(request, semantic_media_files);
let omni_request = OmniGenImageCostAndGenerateRequest {
idempotency_token: Some(uuid_idempotency_token),
model: Some(omni_api_model),
prompt: request.prompt.clone(),
image_media_tokens,
resolution: request.resolution,
aspect_ratio: request.aspect_ratio,
quality: request.quality,
image_batch_count: request.batch_size.map(|n| n as u16),
adjust_horizontal_angle: request.adjust_horizontal_angle,
adjust_vertical_angle: request.adjust_vertical_angle,
adjust_zoom: request.adjust_zoom,
};
info!("Sending image generation via omni endpoint: model={:?}", omni_api_model);
let response = omni_gen_image_generate(
&app_env_configs.storyteller_host,
Some(creds),
omni_request,
).await.map_err(|err| {
error!("Omni image generation failed: {:?}", err);
GenerateError::from(err)
})?;
info!("Omni image generation succeeded: job_token={}", response.inference_job_token.as_str());
let generation_model = tauri_image_model_to_generation_model(tauri_model);
Ok(TaskEnqueueSuccess {
task_type: TaskType::ImageGeneration,
model: Some(generation_model),
provider: GenerationProvider::Artcraft,
provider_job_id: Some(response.inference_job_token.to_string()),
})
}
fn get_image_media_tokens(
request: &TauriGenerateImageRequest,
semantic_media_files: &SemanticMediaFiles,
) -> Option<Vec<MediaFileToken>> {
let num_images = request.image_media_tokens.as_deref()
.map(|t| t.len())
.unwrap_or(0);
let mut image_media_tokens = Vec::with_capacity(num_images + 2);
if let Some(canvas_token) = &semantic_media_files.canvas_image_media_token {
image_media_tokens.push(canvas_token.clone());
}
if let Some(scene_token) = &semantic_media_files.scene_image_media_token {
image_media_tokens.push(scene_token.clone());
}
if let Some(media_tokens) = &request.image_media_tokens {
image_media_tokens.extend(media_tokens.clone());
}
if image_media_tokens.is_empty() {
None
} else {
Some(image_media_tokens)
}
}
@@ -0,0 +1,93 @@
use log::{error, info};
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_router::api::common_image_model::CommonImageModel;
use artcraft_router::api::image_list_ref::ImageListRef;
use artcraft_router::api::provider::Provider;
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
use artcraft_router::client::router_client::RouterClient;
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
/// Handle Flux2LoraAngles image editing via the artcraft_router.
pub async fn handle_flux_2_lora_angles(
request: &TauriGenerateImageRequest,
semantic_media_files: &SemanticMediaFiles,
creds: &StorytellerCredentialSet,
app_env_configs: &AppEnvConfigs,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let client = RouterClient::Artcraft(RouterArtcraftClient::new(
app_env_configs.storyteller_host.clone(),
creds.clone(),
));
let image_inputs = build_image_inputs(request, semantic_media_files);
let router_request = GenerateImageRequest {
model: CommonImageModel::Flux2LoraAngles,
provider: Provider::Artcraft,
prompt: request.prompt.clone(),
image_inputs,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: request.batch_size.map(|n| n as u16),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: request.adjust_horizontal_angle,
vertical_angle: request.adjust_vertical_angle,
zoom: request.adjust_zoom,
};
let plan = router_request.build()?;
info!("Flux2LoraAngles plan: {:?}", plan);
let response = plan.generate_image(&client).await.map_err(|err| {
error!("Flux2LoraAngles generation failed: {:?}", err);
GenerateError::from(err)
})?;
let job_id = response
.get_artcraft_payload()
.map(|p| p.inference_job_token.to_string())
.ok_or(GenerateError::ResponseHadNoJobTokens)?;
info!("Flux2LoraAngles succeeded: job_id={}", job_id);
Ok(TaskEnqueueSuccess {
task_type: TaskType::ImageGeneration,
model: Some(GenerationModel::Flux2LoraAngles),
provider: GenerationProvider::Artcraft,
provider_job_id: Some(job_id),
})
}
fn build_image_inputs(
request: &TauriGenerateImageRequest,
semantic_media_files: &SemanticMediaFiles,
) -> Option<ImageListRef> {
let mut tokens = Vec::new();
if let Some(scene_token) = &semantic_media_files.scene_image_media_token {
tokens.push(scene_token.clone());
}
if let Some(canvas_token) = &semantic_media_files.canvas_image_media_token {
tokens.push(canvas_token.clone());
}
if let Some(media_tokens) = &request.image_media_tokens {
tokens.extend(media_tokens.clone());
}
if tokens.is_empty() { None } else { Some(ImageListRef::MediaFileTokens(tokens)) }
}
@@ -0,0 +1,98 @@
use log::{error, info};
use artcraft_api_defs::generate::image::inpaint::flux_dev_juggernaut_inpaint_image::{
FluxDevJuggernautInpaintImageNumImages, FluxDevJuggernautInpaintImageRequest,
};
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_client::endpoints::generate::image::inpaint::flux_dev_juggernaut_inpaint_image::flux_dev_juggernaut_inpaint_image;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use uuid_utils::uuid::generate_random_uuid;
use tokens::tokens::media_files::MediaFileToken;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
/// Handle FluxDevJuggernaut inpainting via the legacy dedicated endpoint.
///
/// Requires a source image (from scene, canvas, or image_media_tokens) and an inpainting mask.
pub async fn handle_flux_dev_juggernaut_inpaint(
request: &TauriGenerateImageRequest,
semantic_media_files: &SemanticMediaFiles,
creds: &StorytellerCredentialSet,
app_env_configs: &AppEnvConfigs,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let image_media_token = get_first_image_token(request, semantic_media_files)
.ok_or(GenerateError::required_source_image_not_provided())?;
let mask_media_token = semantic_media_files.inpainting_mask_image_media_token.clone()
.ok_or(GenerateError::required_source_image_mask_not_provided())?;
let num_images = match request.batch_size {
None => None,
Some(1) => Some(FluxDevJuggernautInpaintImageNumImages::One),
Some(2) => Some(FluxDevJuggernautInpaintImageNumImages::Two),
Some(3) => Some(FluxDevJuggernautInpaintImageNumImages::Three),
Some(4) => Some(FluxDevJuggernautInpaintImageNumImages::Four),
Some(other) => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
}));
}
};
let uuid_idempotency_token = generate_random_uuid();
let inpaint_request = FluxDevJuggernautInpaintImageRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
image_media_token,
mask_media_token,
num_images,
};
info!("Calling FluxDevJuggernaut inpaint...");
let response = flux_dev_juggernaut_inpaint_image(
&app_env_configs.storyteller_host,
Some(creds),
inpaint_request,
).await.map_err(|err| {
error!("FluxDevJuggernaut inpaint failed: {:?}", err);
GenerateError::from(err)
})?;
info!("FluxDevJuggernaut inpaint succeeded: job_token={}", response.inference_job_token);
Ok(TaskEnqueueSuccess {
task_type: TaskType::ImageInpaintEdit,
model: Some(GenerationModel::FluxDevJuggernaut),
provider: GenerationProvider::Artcraft,
provider_job_id: Some(response.inference_job_token.to_string()),
})
}
fn get_first_image_token(
request: &TauriGenerateImageRequest,
semantic_media_files: &SemanticMediaFiles,
) -> Option<MediaFileToken> {
if let Some(scene_token) = &semantic_media_files.scene_image_media_token {
return Some(scene_token.clone());
}
if let Some(canvas_token) = &semantic_media_files.canvas_image_media_token {
return Some(canvas_token.clone());
}
if let Some(media_tokens) = &request.image_media_tokens {
if let Some(token) = media_tokens.first() {
return Some(token.clone());
}
}
None
}
@@ -0,0 +1,96 @@
use log::{error, info};
use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{
FluxProKontextMaxEditImageNumImages, FluxProKontextMaxEditImageRequest,
};
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_client::endpoints::generate::image::edit::flux_pro_kontext_max_edit_image::flux_pro_kontext_max_edit_image;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use tokens::tokens::media_files::MediaFileToken;
use uuid_utils::uuid::generate_random_uuid;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
/// Handle FluxProKontextMax image editing via the legacy dedicated endpoint.
///
/// Consolidates both the "edit" and "inpaint" legacy paths — both call the same
/// Kontext Max edit endpoint with a single source image.
pub async fn handle_flux_pro_kontext_edit(
request: &TauriGenerateImageRequest,
semantic_media_files: &SemanticMediaFiles,
creds: &StorytellerCredentialSet,
app_env_configs: &AppEnvConfigs,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let image_media_token = get_first_image_token(request, semantic_media_files)
.ok_or(GenerateError::BadInput(BadInputReason::WrongImageArguments(
"No input image specified for Flux Kontext Max edit".to_string(),
)))?;
let num_images = match request.batch_size {
None => None,
Some(1) => Some(FluxProKontextMaxEditImageNumImages::One),
Some(2) => Some(FluxProKontextMaxEditImageNumImages::Two),
Some(3) => Some(FluxProKontextMaxEditImageNumImages::Three),
Some(4) => Some(FluxProKontextMaxEditImageNumImages::Four),
Some(other) => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
}));
}
};
let uuid_idempotency_token = generate_random_uuid();
let kontext_request = FluxProKontextMaxEditImageRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
image_media_token,
num_images,
};
info!("Calling FluxProKontextMax edit...");
let response = flux_pro_kontext_max_edit_image(
&app_env_configs.storyteller_host,
Some(creds),
kontext_request,
).await.map_err(|err| {
error!("FluxProKontextMax edit failed: {:?}", err);
GenerateError::from(err)
})?;
info!("FluxProKontextMax edit succeeded: job_token={}", response.inference_job_token);
Ok(TaskEnqueueSuccess {
task_type: TaskType::ImageGeneration,
model: Some(GenerationModel::FluxProKontextMax),
provider: GenerationProvider::Artcraft,
provider_job_id: Some(response.inference_job_token.to_string()),
})
}
fn get_first_image_token(
request: &TauriGenerateImageRequest,
semantic_media_files: &SemanticMediaFiles,
) -> Option<MediaFileToken> {
if let Some(scene_token) = &semantic_media_files.scene_image_media_token {
return Some(scene_token.clone());
}
if let Some(canvas_token) = &semantic_media_files.canvas_image_media_token {
return Some(canvas_token.clone());
}
if let Some(media_tokens) = &request.image_media_tokens {
if let Some(token) = media_tokens.first() {
return Some(token.clone());
}
}
None
}
@@ -0,0 +1,93 @@
use log::{error, info};
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_router::api::common_image_model::CommonImageModel;
use artcraft_router::api::image_list_ref::ImageListRef;
use artcraft_router::api::provider::Provider;
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
use artcraft_router::client::router_client::RouterClient;
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
/// Handle QwenEdit2511Angles image editing via the artcraft_router.
pub async fn handle_qwen_edit_2511_angles(
request: &TauriGenerateImageRequest,
semantic_media_files: &SemanticMediaFiles,
creds: &StorytellerCredentialSet,
app_env_configs: &AppEnvConfigs,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let client = RouterClient::Artcraft(RouterArtcraftClient::new(
app_env_configs.storyteller_host.clone(),
creds.clone(),
));
let image_inputs = build_image_inputs(request, semantic_media_files);
let router_request = GenerateImageRequest {
model: CommonImageModel::QwenEdit2511Angles,
provider: Provider::Artcraft,
prompt: request.prompt.clone(),
image_inputs,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: request.batch_size.map(|n| n as u16),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: request.adjust_horizontal_angle,
vertical_angle: request.adjust_vertical_angle,
zoom: request.adjust_zoom,
};
let plan = router_request.build()?;
info!("QwenEdit2511Angles plan: {:?}", plan);
let response = plan.generate_image(&client).await.map_err(|err| {
error!("QwenEdit2511Angles generation failed: {:?}", err);
GenerateError::from(err)
})?;
let job_id = response
.get_artcraft_payload()
.map(|p| p.inference_job_token.to_string())
.ok_or(GenerateError::ResponseHadNoJobTokens)?;
info!("QwenEdit2511Angles succeeded: job_id={}", job_id);
Ok(TaskEnqueueSuccess {
task_type: TaskType::ImageGeneration,
model: Some(GenerationModel::QwenEdit2511Angles),
provider: GenerationProvider::Artcraft,
provider_job_id: Some(job_id),
})
}
fn build_image_inputs(
request: &TauriGenerateImageRequest,
semantic_media_files: &SemanticMediaFiles,
) -> Option<ImageListRef> {
let mut tokens = Vec::new();
if let Some(scene_token) = &semantic_media_files.scene_image_media_token {
tokens.push(scene_token.clone());
}
if let Some(canvas_token) = &semantic_media_files.canvas_image_media_token {
tokens.push(canvas_token.clone());
}
if let Some(media_tokens) = &request.image_media_tokens {
tokens.extend(media_tokens.clone());
}
if tokens.is_empty() { None } else { Some(ImageListRef::MediaFileTokens(tokens)) }
}
@@ -0,0 +1,4 @@
pub mod artcraft_flux_2_lora_angles;
pub mod artcraft_flux_dev_juggernaut_inpaint;
pub mod artcraft_flux_pro_kontext_edit;
pub mod artcraft_qwen_edit_2511_angles;
@@ -0,0 +1,6 @@
mod handle_artcraft;
mod handle_artcraft_via_legacy;
mod handle_artcraft_via_omni_endpoint;
mod legacy;
pub use handle_artcraft::handle_artcraft;
@@ -0,0 +1,92 @@
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image::aspect_ratio_to_grok_image;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
use crate::services::grok::state::grok_image_prompt_queue::{GrokImagePromptQueue, PromptItem};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use grok_client::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio;
use uuid_utils::uuid::generate_random_uuid;
use tauri::AppHandle;
pub async fn handle_grok(
app: &AppHandle,
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
creds_manager: &GrokCredentialManager,
grok_image_prompt_queue: &GrokImagePromptQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
//// TODO: We can request population of the user info if absent or expired.
//let websocket = get_websocket(
// app,
// creds_manager,
// websocket_manager,
//).await?;
//
//let _result = prompt_websocket_image(PromptWebsocketImageArgs {
// websocket_wrapped: &websocket,
// prompt: request.prompt.as_deref().unwrap_or_else(||""),
//}).await?;
//
//let request_id = listen_for_websocket_request_id(ListenForWebsocketRequestIdArgs {
// websocket: &websocket,
// timeout: Duration::from_millis(10_000),
//}).await?;
//
//let request_id = match request_id.request_id {
// Some(request_id) => request_id.to_string(),
// None => {
// warn!("No request id from Grok image generation (websocket)");
// return Err(GenerateError::ProviderFailure(ProviderFailureReason::GrokJobEnqueueFailed));
// }
//};
//
//info!("Successfully enqueued MidJourney. Job token: {}", request_id);
let job_id = generate_random_uuid();
let prompt = request.prompt
.as_deref()
.map(|prompt| prompt.trim().to_string())
.unwrap_or_else(|| "".to_string());
let aspect_ratio = get_aspect_ratio(request);
grok_image_prompt_queue.enqueue(PromptItem {
task_id: job_id.clone(),
prompt,
aspect_ratio: aspect_ratio.unwrap_or(ClientMessageAspectRatio::Square),
})?;
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Grok,
model: Some(GenerationModel::GrokImage),
provider_job_id: Some(job_id),
task_type: TaskType::ImageGeneration,
})
}
fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<ClientMessageAspectRatio> {
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
// Handle modern aspect ratio
let aspect = aspect_ratio_to_grok_image(common_aspect_ratio);
return Some(aspect);
}
if let Some(aspect_ratio) = request.aspect_ratio {
// Handle deprecated aspect ratio
return match aspect_ratio {
TextToImageSize::Auto => Some(ClientMessageAspectRatio::Square),
TextToImageSize::Square => Some(ClientMessageAspectRatio::Square),
TextToImageSize::Wide => Some(ClientMessageAspectRatio::WideThreeByTwo),
TextToImageSize::Tall => Some(ClientMessageAspectRatio::TallTwoByThree),
}
}
None
}
@@ -0,0 +1 @@
pub (super) mod handle_grok;
@@ -0,0 +1,140 @@
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::canvas_background_removal_complete_event::CanvasBackgroundRemovalCompleteEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
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::warning_events::flash_user_input_error_event::FlashUserInputErrorEvent;
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::provider_priority::ProviderPriorityStore;
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
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 enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use uuid_utils::uuid::generate_random_uuid;
use log::{error, info};
use midjourney_client::client::midjourney_hostname::MidjourneyHostname;
use midjourney_client::endpoints::submit_job::{submit_job, SubmitJobRequest};
use midjourney_client::error::midjourney_api_error::MidjourneyApiError;
use midjourney_client::recipes::channel_id::ChannelId;
use midjourney_client::recipes::text_to_image::{text_to_image, TextToImageError, TextToImageRequest};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use std::time::Duration;
use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub async fn handle_midjourney(
app: &AppHandle,
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
mj_creds_manager: &MidjourneyCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match mj_creds_manager.maybe_copy_cookie_store() {
Ok(Some(creds)) => creds,
Ok(None) => {
error!("Midjourney credentials not found.");
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Midjourney, &app);
return Err(GenerateError::needs_midjourney_credentials());
}
Err(err) => {
error!("Error reading Midjourney credentials: {:?}", err);
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Midjourney, &app);
return Err(GenerateError::needs_midjourney_credentials());
},
};
// TODO: We can request population of the user info if absent or expired.
let user_info = match mj_creds_manager.maybe_copy_user_info() {
Ok(Some(user_info)) => user_info,
Ok(None) => {
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserInfo));
}
Err(err) => {
error!("Error reading Midjourney user info: {:?}", err);
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserInfo));
},
};
let channel_id = match user_info.user_id {
Some(user_id) => ChannelId::UserId(user_id),
None => {
error!("Midjourney user info does not contain a user ID.");
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserId));
}
};
info!("Calling midjourney ...");
let cookie_header = creds.to_cookie_string();
let prompt = request.prompt
.as_deref()
.unwrap_or("");
let result = text_to_image(TextToImageRequest {
prompt,
channel_id: &channel_id,
hostname: MidjourneyHostname::Standard,
cookie_header,
}).await;
let result = match result {
Ok(result) => result,
Err(err) => {
error!("Failed to use MidJourney: {:?}", err);
return Err(GenerateError::from(err));
}
};
let job_id = match result.maybe_job_id {
Some(job_id) => job_id,
None => {
error!("Failed to enqueue MidJourney: No job ID returned.");
return handle_midjourney_errors(app, result.maybe_errors);
}
};
info!("Successfully enqueued MidJourney. Job token: {}", job_id);
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Midjourney,
model: Some(GenerationModel::Midjourney),
provider_job_id: Some(job_id),
task_type: TaskType::ImageGeneration,
})
}
fn handle_midjourney_errors(
app: &AppHandle,
maybe_errors: Option<Vec<TextToImageError>>
) -> Result<TaskEnqueueSuccess, GenerateError> {
if let Some(errors) = maybe_errors {
if !errors.is_empty() {
let messages: Vec<String> = errors.iter()
.map(|e| format!("{:?}", e))
.collect();
let combined_message = messages.join("; ");
let event = FlashUserInputErrorEvent {
message: format!("Midjourney Error: {}", combined_message),
};
if let Err(err) = event.send(&app) {
error!("Failed to send FlashUserInputErrorEvent: {:?}", err); // Fail open
}
}
}
Err(GenerateError::ProviderFailure(ProviderFailureReason::MidjourneyJobEnqueueFailed))
}
@@ -0,0 +1 @@
pub (super) mod handle_midjourney;
@@ -0,0 +1 @@
pub mod artcraft;
@@ -0,0 +1,135 @@
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image::aspect_ratio_to_grok_image;
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_sora_native_gpt_image_1::aspect_ratio_to_sora_native_gpt_image_1;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use errors::AnyhowResult;
use grok_client::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio;
use log::{error, info};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use std::time::Duration;
use tauri::AppHandle;
const SORA_SIMPLE_IMAGE_GEN_TIMEOUT : Duration = Duration::from_millis(1000 * 30); // 30 seconds
pub async fn handle_gpt_image_1_sora_text_to_image(
request: &EnqueueTextToImageRequest,
app: &AppHandle,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let mut creds = match sora_creds_manager.get_credentials() {
Ok(Some(creds)) => creds,
Ok(None) => {
error!("Sora credentials not found.");
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Sora, &app);
return Err(GenerateError::needs_sora_credentials());
}
Err(err) => {
error!("Error reading Sora credentials: {:?}", err);
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Sora, &app);
return Err(GenerateError::needs_sora_credentials());
}
};
let credential_updated = maybe_upgrade_or_renew_session(&mut creds)
.await
.map_err(|err| {
error!("Failed to upgrade or renew session: {:?}", err);
err
})?;
if credential_updated {
info!("Storing updated credentials");
sora_creds_manager.set_credentials(&creds)?;
}
let prompt = request.prompt.as_deref().unwrap_or("");
let aspect_ratio = get_aspect_ratio(request)
.unwrap_or(ImageSize::Square);
let num_images = match request.number_images {
None => NumImages::One,
Some(1) => NumImages::One,
Some(2) => NumImages::Two,
Some(3) => NumImages::Two, // NB: There is no option for three images!
Some(4) => NumImages::Four,
_ => NumImages::One,
};
let result =
simple_image_gen_with_session_auto_renew(SimpleImageGenAutoRenewRequest {
prompt: prompt.to_string(),
num_images,
image_size: aspect_ratio,
credentials: &creds,
request_timeout: Some(SORA_SIMPLE_IMAGE_GEN_TIMEOUT),
}).await;
let (response, maybe_new_creds) =
match result {
Ok((response, maybe_new_creds)) => (response, maybe_new_creds),
Err(err) => {
let event = GenerationEnqueueFailureEvent {
action: GenerationAction::GenerateImage,
service: GenerationServiceProvider::Sora,
model: None,
reason: None,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(GenerateError::from(err));
}
};
if let Some(new_creds) = maybe_new_creds {
info!("Storing updated credentials.");
sora_creds_manager.set_credentials(&new_creds)?;
}
info!("New Sora Task ID: {:?} ", response.task_id);
sora_task_queue.insert(&response.task_id)?;
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Sora,
task_type: TaskType::ImageGeneration,
model: Some(GenerationModel::GptImage1),
provider_job_id: Some(response.task_id.to_string()),
})
}
fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<ImageSize> {
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
// Handle modern aspect ratio
let aspect = aspect_ratio_to_sora_native_gpt_image_1(common_aspect_ratio);
return Some(aspect);
}
if let Some(aspect_ratio) = request.aspect_ratio {
// Handle deprecated aspect ratio
return match aspect_ratio {
TextToImageSize::Auto => Some(ImageSize::Square),
TextToImageSize::Square => Some(ImageSize::Square),
TextToImageSize::Wide => Some(ImageSize::Wide),
TextToImageSize::Tall => Some(ImageSize::Tall),
}
}
None
}
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
use crate::core::commands::enqueue::text_to_image::sora::handle_gpt_image_1_sora_text_to_image::handle_gpt_image_1_sora_text_to_image;
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
use crate::core::commands::generate::generate_image::sora::handle_gpt_image_1_sora_text_to_image::handle_gpt_image_1_sora_text_to_image;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use tauri::AppHandle;
@@ -0,0 +1,2 @@
pub (super) mod handle_text_to_image_sora;
mod handle_gpt_image_1_sora_text_to_image;
@@ -0,0 +1,123 @@
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
use enums::common::generation::common_resolution::CommonResolution;
use enums::common::generation::common_quality::CommonQuality;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
// ── Request ──
#[derive(Deserialize, Debug)]
pub struct TauriGenerateImageRequest {
/// The provider to use (defaults to Artcraft/Storyteller).
/// Not all (provider, model) combinations are valid.
pub provider: Option<GenerationProvider>,
/// The model to use.
pub model: Option<TauriImageModel>,
/// Text prompt for the image generation.
pub prompt: Option<String>,
/// Aspect ratio.
pub aspect_ratio: Option<CommonAspectRatio>,
/// Resolution.
pub resolution: Option<CommonResolution>,
/// Quality (used by OpenAI models).
pub quality: Option<CommonQuality>,
/// The number of images to generate.
pub batch_size: Option<u32>,
/// Reference images (without semantics).
/// The purpose varies on a model-by-model basis.
pub image_media_tokens: Option<Vec<MediaFileToken>>,
// ── Canvas / scene images ──
/// Supply this *XOR* `canvas_image_raw_bytes`.
/// Becomes the first image reference (pushing back `image_media_tokens` by one).
pub canvas_image_media_token: Option<MediaFileToken>,
/// Supply this *XOR* `canvas_image_media_token`.
/// Raw bytes of a canvas image.
pub canvas_image_raw_bytes: Option<Vec<u8>>,
/// Supply this *XOR* `scene_image_raw_bytes`.
/// Becomes a scene reference image.
pub scene_image_media_token: Option<MediaFileToken>,
/// Supply this *XOR* `scene_image_media_token`.
/// Raw bytes of a scene image.
pub scene_image_raw_bytes: Option<Vec<u8>>,
// ── Inpainting ──
/// Supply this *XOR* `inpainting_mask_image_raw_bytes`.
/// The mask to focus the edit (already uploaded).
pub inpainting_mask_image_media_token: Option<MediaFileToken>,
/// Supply this *XOR* `inpainting_mask_image_media_token`.
/// The mask to focus the edit (raw bytes).
pub inpainting_mask_image_raw_bytes: Option<Vec<u8>>,
// ── Angle adjustment (for edit models like QwenEdit, Flux2LoraAngles) ──
/// Horizontal angle adjustment.
pub adjust_horizontal_angle: Option<f64>,
/// Vertical angle adjustment.
pub adjust_vertical_angle: Option<f64>,
/// Zoom adjustment.
pub adjust_zoom: Option<f64>,
/// Turn on the system prompt.
pub enable_system_prompt: Option<bool>,
// ── Frontend metadata ──
/// Name of the frontend caller.
pub frontend_caller: Option<TauriCommandCaller>,
/// A frontend-defined identifier sent back as a Tauri event on task completion.
pub frontend_subscriber_id: Option<String>,
/// A frontend-defined payload sent back as a Tauri event on task completion.
pub frontend_subscriber_payload: Option<String>,
}
// ── Response ──
#[derive(Serialize)]
pub struct TauriGenerateImageResponse {
}
impl SerializeMarker for TauriGenerateImageResponse {}
// ── Error ──
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum TauriGenerateImageErrorType {
/// Caller didn't specify a model
ModelNotSpecified,
/// Bad input
BadInput,
/// No provider available
NoProviderAvailable,
/// Generic server error
ServerError,
/// Needs to be logged into Artcraft
NeedsStorytellerCredentials,
/// Needs Grok credentials
NeedsGrokCredentials,
/// Billing issue
BillingIssue,
}
@@ -0,0 +1,92 @@
use enums::common::generation::common_model_type::CommonModelType;
use serde_derive::Deserialize;
/// Unified image model enum covering text-to-image, image edit, and inpainting.
///
/// 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 TauriImageModel {
// Text-to-image models
#[serde(rename = "flux_1_dev")]
Flux1Dev,
#[serde(rename = "flux_1_schnell")]
Flux1Schnell,
#[serde(rename = "flux_pro_11")]
FluxPro11,
#[serde(rename = "flux_pro_11_ultra")]
FluxPro11Ultra,
#[serde(rename = "grok_image")]
GrokImage,
#[serde(rename = "recraft_3")]
Recraft3,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "gpt_image_1p5")]
GptImage1p5,
#[serde(rename = "gpt_image_2")]
GptImage2,
#[serde(rename = "gemini_25_flash")]
Gemini25Flash,
#[serde(rename = "nano_banana")]
NanoBanana,
#[serde(rename = "nano_banana_2")]
NanoBanana2,
#[serde(rename = "nano_banana_pro")]
NanoBananaPro,
#[serde(rename = "seedream_4")]
Seedream4,
#[serde(rename = "seedream_4p5")]
Seedream4p5,
#[serde(rename = "seedream_5_lite")]
Seedream5Lite,
#[serde(rename = "midjourney")]
Midjourney,
// Image edit models
#[serde(rename = "flux_pro_kontext_max")]
FluxProKontextMax,
#[serde(rename = "qwen_edit_2511_angles")]
QwenEdit2511Angles,
#[serde(rename = "flux_2_lora_angles")]
Flux2LoraAngles,
// Inpainting models
#[serde(rename = "flux_dev_juggernaut")]
FluxDevJuggernaut,
#[serde(rename = "flux_pro_1")]
FluxPro1,
}
impl TauriImageModel {
pub fn to_common_model_type(&self) -> CommonModelType {
match self {
Self::Flux1Dev => CommonModelType::Flux1Dev,
Self::Flux1Schnell => CommonModelType::Flux1Schnell,
Self::FluxPro11 => CommonModelType::FluxPro11,
Self::FluxPro11Ultra => CommonModelType::FluxPro11Ultra,
Self::GrokImage => CommonModelType::GrokImage,
Self::Recraft3 => CommonModelType::Recraft3,
Self::GptImage1 => CommonModelType::GptImage1,
Self::GptImage1p5 => CommonModelType::GptImage1p5,
Self::GptImage2 => CommonModelType::GptImage2,
Self::Gemini25Flash => CommonModelType::NanoBanana,
Self::NanoBanana => CommonModelType::NanoBanana,
Self::NanoBanana2 => CommonModelType::NanoBanana2,
Self::NanoBananaPro => CommonModelType::NanoBananaPro,
Self::Seedream4 => CommonModelType::Seedream4,
Self::Seedream4p5 => CommonModelType::Seedream4p5,
Self::Seedream5Lite => CommonModelType::Seedream5Lite,
Self::Midjourney => CommonModelType::Midjourney,
Self::FluxProKontextMax => CommonModelType::FluxProKontextMax,
Self::QwenEdit2511Angles => CommonModelType::QwenEdit2511Angles,
Self::Flux2LoraAngles => CommonModelType::Flux2LoraAngles,
Self::FluxDevJuggernaut => CommonModelType::FluxDevJuggernaut,
Self::FluxPro1 => CommonModelType::FluxPro1,
}
}
}
@@ -0,0 +1 @@
pub mod parse_semantic_media_files;
@@ -0,0 +1,123 @@
use log::info;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_client::utils::api_host::ApiHost;
use tokens::tokens::media_files::MediaFileToken;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::core::utils::upload_bytes_to_media_file::upload_image_bytes_as_media_file::upload_image_bytes_as_media_file;
use crate::core::utils::upload_bytes_to_media_file::upload_mask_image_bytes_as_media_file::upload_mask_image_bytes_as_media_file;
/// Resolved semantic media file tokens for canvas, scene, and inpainting mask.
///
/// Each field is populated either from an already-uploaded token on the request
/// or by uploading raw bytes on the fly.
pub struct SemanticMediaFiles {
pub canvas_image_media_token: Option<MediaFileToken>,
pub scene_image_media_token: Option<MediaFileToken>,
pub inpainting_mask_image_media_token: Option<MediaFileToken>,
}
/// Resolve semantic media files from the request.
///
/// For each of (canvas, scene, mask): the request may supply a pre-uploaded
/// token XOR raw bytes. If both are supplied, returns an error. If raw bytes
/// are supplied, uploads them and returns the resulting token.
pub async fn parse_semantic_media_files(
request: &TauriGenerateImageRequest,
creds: &StorytellerCredentialSet,
api_host: &ApiHost,
) -> Result<SemanticMediaFiles, GenerateError> {
// Canvas image
let canvas_image_media_token = resolve_image_field(
"canvas_image",
request.canvas_image_media_token.as_ref(),
request.canvas_image_raw_bytes.as_deref(),
creds,
api_host,
).await?;
// Scene image
let scene_image_media_token = resolve_image_field(
"scene_image",
request.scene_image_media_token.as_ref(),
request.scene_image_raw_bytes.as_deref(),
creds,
api_host,
).await?;
// Inpainting mask
let inpainting_mask_image_media_token = resolve_mask_field(
request.inpainting_mask_image_media_token.as_ref(),
request.inpainting_mask_image_raw_bytes.as_deref(),
creds,
api_host,
).await?;
Ok(SemanticMediaFiles {
canvas_image_media_token,
scene_image_media_token,
inpainting_mask_image_media_token,
})
}
// ── Helpers ──
async fn resolve_image_field(
field_name: &str,
maybe_token: Option<&MediaFileToken>,
maybe_raw_bytes: Option<&[u8]>,
creds: &StorytellerCredentialSet,
api_host: &ApiHost,
) -> Result<Option<MediaFileToken>, GenerateError> {
if maybe_token.is_some() && maybe_raw_bytes.is_some() {
return Err(GenerateError::BadInput(BadInputReason::WrongImageArguments(format!(
"Cannot supply both {}_media_token and {}_raw_bytes",
field_name, field_name
))));
}
if let Some(token) = maybe_token {
return Ok(Some(token.clone()));
}
if let Some(raw_bytes) = maybe_raw_bytes {
info!("Uploading {} raw bytes ({} bytes)...", field_name, raw_bytes.len());
let token = upload_image_bytes_as_media_file(
creds,
api_host,
raw_bytes.to_vec(),
).await?;
return Ok(Some(token));
}
Ok(None)
}
async fn resolve_mask_field(
maybe_token: Option<&MediaFileToken>,
maybe_raw_bytes: Option<&[u8]>,
creds: &StorytellerCredentialSet,
api_host: &ApiHost,
) -> Result<Option<MediaFileToken>, GenerateError> {
if maybe_token.is_some() && maybe_raw_bytes.is_some() {
return Err(GenerateError::BadInput(BadInputReason::BothImageMaskMediaTokenAndBytesSupplied));
}
if let Some(token) = maybe_token {
return Ok(Some(token.clone()));
}
if let Some(raw_bytes) = maybe_raw_bytes {
info!("Uploading inpainting mask raw bytes ({} bytes)...", raw_bytes.len());
let token = upload_mask_image_bytes_as_media_file(
creds,
api_host,
raw_bytes,
).await?;
return Ok(Some(token));
}
Ok(None)
}
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -26,7 +26,7 @@ use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub (super) async fn handle_artcraft_kling_1p6_pro(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -26,7 +26,7 @@ use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub (super) async fn handle_artcraft_kling_2p1_master(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -25,7 +25,7 @@ use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub (super) async fn handle_artcraft_kling_2p1_pro(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -24,7 +24,7 @@ use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub (super) async fn handle_artcraft_kling_2p5_turbo_pro(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -26,7 +26,7 @@ use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub (super) async fn handle_artcraft_kling_2p6_pro(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -27,7 +27,7 @@ use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub (super) async fn handle_artcraft_seedance_1_lite(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -28,7 +28,7 @@ use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub (super) async fn handle_artcraft_sora2(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -27,7 +27,7 @@ use artcraft_client::credentials::storyteller_credential_set::StorytellerCredent
use tokens::tokens::media_files::MediaFileToken;
pub (super) async fn handle_artcraft_sora2_pro(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -25,7 +25,7 @@ use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub (super) async fn handle_artcraft_veo2(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -25,7 +25,7 @@ use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub (super) async fn handle_artcraft_veo3(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -26,7 +26,7 @@ use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub (super) async fn handle_artcraft_veo3_fast(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -24,7 +24,7 @@ use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub (super) async fn handle_artcraft_veo3p1(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
@@ -24,7 +24,7 @@ use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub (super) async fn handle_artcraft_veo3p1_fast(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
@@ -0,0 +1,65 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_kling_1p6_pro::handle_artcraft_kling_1p6_pro;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_kling_2p1_master::handle_artcraft_kling_2p1_master;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_kling_2p1_pro::handle_artcraft_kling_2p1_pro;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_kling_2p5_turbo_pro::handle_artcraft_kling_2p5_turbo_pro;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_kling_2p6_pro::handle_artcraft_kling_2p6_pro;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_seedance_1_lite::handle_artcraft_seedance_1_lite;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_sora2::handle_artcraft_sora2;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_sora2_pro::handle_artcraft_sora2_pro;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_veo2::handle_artcraft_veo2;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_veo3::handle_artcraft_veo3;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_veo3_fast::handle_artcraft_veo3_fast;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_veo3p1::handle_artcraft_veo3p1;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_veo3p1_fast::handle_artcraft_veo3p1_fast;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_video_via_router::handle_artcraft_video_via_router;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, TauriVideoModel};
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use artcraft_router::api::common_video_model::CommonVideoModel;
use tauri::AppHandle;
pub async fn handle_video_artcraft(
request: &TauriGenerateVideoRequest,
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = get_storyteller_creds_or_error(app, storyteller_creds_manager)?;
match request.model {
None => {
Err(GenerateError::no_model_specified())
},
// Handled via router
Some(TauriVideoModel::Kling3p0Pro) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Kling3p0Pro, GenerationModel::Kling3p0Pro).await,
Some(TauriVideoModel::Kling3p0Standard) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Kling3p0Standard, GenerationModel::Kling3p0Standard).await,
Some(TauriVideoModel::Seedance1p5Pro) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Seedance1p5Pro, GenerationModel::Seedance1p5Pro).await,
Some(TauriVideoModel::Seedance2p0) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Seedance2p0, GenerationModel::Seedance2p0).await,
Some(TauriVideoModel::Seedance2p0Fast) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Seedance2p0Fast, GenerationModel::Seedance2p0Fast).await,
Some(TauriVideoModel::HappyHorse1p0) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::HappyHorse1p0, GenerationModel::HappyHorse1p0).await,
// Legacy endpoints
Some(TauriVideoModel::Kling16Pro) => handle_artcraft_kling_1p6_pro(request, app_env_configs, &creds).await,
Some(TauriVideoModel::Kling21Pro) => handle_artcraft_kling_2p1_pro(request, app_env_configs, &creds).await,
Some(TauriVideoModel::Kling21Master) => handle_artcraft_kling_2p1_master(request, app_env_configs, &creds).await,
Some(TauriVideoModel::Kling2p5TurboPro) => handle_artcraft_kling_2p5_turbo_pro(request, app_env_configs, &creds).await,
Some(TauriVideoModel::Kling2p6Pro) => handle_artcraft_kling_2p6_pro(request, app_env_configs, &creds).await,
Some(TauriVideoModel::Seedance10Lite) => handle_artcraft_seedance_1_lite(request, app_env_configs, &creds).await,
Some(TauriVideoModel::Sora2) => handle_artcraft_sora2(request, app_env_configs, &creds).await,
Some(TauriVideoModel::Sora2Pro) => handle_artcraft_sora2_pro(request, app_env_configs, &creds).await,
Some(TauriVideoModel::Veo2) => handle_artcraft_veo2(request, app_env_configs, &creds).await,
Some(TauriVideoModel::Veo3) => handle_artcraft_veo3(request, app_env_configs, &creds).await,
Some(TauriVideoModel::Veo3Fast) => handle_artcraft_veo3_fast(request, app_env_configs, &creds).await,
Some(TauriVideoModel::Veo3p1) => handle_artcraft_veo3p1(request, app_env_configs, &creds).await,
Some(TauriVideoModel::Veo3p1Fast) => handle_artcraft_veo3p1_fast(request, app_env_configs, &creds).await,
Some(_) => {
Err(GenerateError::AnyhowError(
anyhow!("wrong logic: another branch should handle this: {:?}", request.model)))
},
}
}
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
use crate::core::commands::generate::generate_video::request::TauriGenerateVideoRequest;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
@@ -22,7 +22,7 @@ use enums::tauri::tasks::task_type::TaskType;
use log::{error, info};
pub(super) async fn handle_artcraft_video_via_router(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
model: CommonVideoModel,
@@ -6,7 +6,6 @@ mod handle_artcraft_kling_2p5_turbo_pro;
mod handle_artcraft_kling_2p6_pro;
mod handle_artcraft_video_via_router;
mod handle_artcraft_seedance_1_lite;
mod handle_artcraft_seedance_2p0;
mod handle_artcraft_sora2;
mod handle_artcraft_sora2_pro;
mod handle_artcraft_veo2;
@@ -0,0 +1,210 @@
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_video::handle_video_artcraft;
use crate::core::commands::generate::generate_video::grok::handle_grok_video::handle_grok_video;
use crate::core::commands::generate::generate_video::request::{
TauriGenerateVideoErrorType, TauriGenerateVideoRequest, TauriGenerateVideoResponse,
TauriVideoModel,
};
use crate::core::commands::generate::generate_video::sora2::handle_sora_sora2::handle_sora_sora2;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
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::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::state::task_database::TaskDatabase;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
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 enums::common::generation_provider::GenerationProvider;
use log::{error, info, warn};
use tauri::{AppHandle, State};
#[tauri::command]
pub async fn generate_video_command(
mut request: TauriGenerateVideoRequest,
app: AppHandle,
app_env_configs: State<'_, AppEnvConfigs>,
app_data_root: State<'_, AppDataRoot>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
grok_creds_manager: State<'_, GrokCredentialManager>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
sora_creds_manager: State<'_, SoraCredentialManager>,
) -> Response<TauriGenerateVideoResponse, TauriGenerateVideoErrorType, ()> {
info!("generate_video_command called, request: {:?}", request);
if request.image_media_token.is_none()
&& request.start_frame_image_media_token.is_some() {
// NB: This is to fix the legacy handlers
request.image_media_token = request.start_frame_image_media_token.clone();
}
if request.image_media_token.is_some()
&& request.start_frame_image_media_token.is_none() {
// NB: This is to fix the modern handlers
request.start_frame_image_media_token = request.image_media_token.clone();
}
let result = handle_request(
request,
&app,
&app_env_configs,
&app_data_root,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&grok_creds_manager,
&sora_creds_manager,
&storyteller_creds_manager,
).await;
match result {
Err(err) => {
error!("error: {:?}", err);
notify_frontend_of_errors(&app, &err).await;
let mut status = CommandErrorStatus::ServerError;
let mut error_type = TauriGenerateVideoErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
match err {
GenerateError::BadInput(BadInputReason::NoModelSpecified) => {
status = CommandErrorStatus::BadRequest;
error_type = TauriGenerateVideoErrorType::ModelNotSpecified;
error_message = "No model specified for video generation";
}
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = TauriGenerateVideoErrorType::NoProviderAvailable;
error_message = "No configured provider available for video generation";
}
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey) => {
status = CommandErrorStatus::Unauthorized;
error_type = TauriGenerateVideoErrorType::NeedsFalApiKey;
error_message = "You need to set a FAL api key";
},
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials) => {
status = CommandErrorStatus::Unauthorized;
error_type = TauriGenerateVideoErrorType::NeedsStorytellerCredentials;
error_message = "You need to be logged into Artcraft.";
}
_ => {}, // Fall-through
}
Err(CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
})
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
action: event.to_frontend_event_action(),
service: event.to_frontend_event_service(),
model: event.model,
};
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err);
}
CreditsBalanceChangedEvent{}.send_infallible(&app);
Ok(TauriGenerateVideoResponse {}.into())
}
}
}
async fn handle_request(
request: TauriGenerateVideoRequest,
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
grok_creds_manager: &GrokCredentialManager,
sora_creds_manager: &SoraCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let model = match request.model {
Some(model) => model,
None => {
return Err(GenerateError::no_model_specified())
}
};
let provider = match (model, request.provider) {
(TauriVideoModel::GrokVideo, _) => GenerationProvider::Grok,
_ => request.provider.unwrap_or(GenerationProvider::Artcraft),
};
info!("generate video with {:?} via provider {:?}", &model, &provider);
let result = match provider {
GenerationProvider::Grok => {
handle_grok_video(
&request,
app,
app_data_root,
app_env_configs,
grok_creds_manager,
).await
}
GenerationProvider::Sora => {
handle_sora_sora2(
&request,
app,
app_data_root,
app_env_configs,
sora_creds_manager,
).await
}
_ => {
handle_video_artcraft(
&request,
app,
app_env_configs,
storyteller_creds_manager,
).await
}
};
let success_event = match result {
Err(err) => return Err(err),
Ok(event) => event,
};
let result = success_event
.insert_into_task_database_with_frontend_payload(
task_database,
request.frontend_caller,
request.frontend_subscriber_id.as_deref(),
request.frontend_subscriber_payload.as_deref()
)
.await;
if let Err(err) = result {
error!("Failed to create task in database: {:?}", err);
}
if let Err(err) = artcraft_usage_tracker.record_video_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::VideoPage) {
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, GrokAspectRatio, SoraOrientation};
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, GrokAspectRatio, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::core::events::generation_events::common::GenerationModel;
@@ -33,7 +33,7 @@ use tauri::AppHandle;
const GROK_IMAGE_UPLOAD_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds
pub async fn handle_grok_video(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
@@ -0,0 +1,5 @@
pub mod generate_video_command;
pub mod request;
mod artcraft;
mod grok;
pub mod sora2;
@@ -0,0 +1,144 @@
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio;
use artcraft_router::api::common_resolution::CommonResolution;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
/// 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 TauriVideoModel {
#[serde(rename = "grok_video")]
GrokVideo,
#[serde(rename = "kling_1.6_pro")]
Kling16Pro,
#[serde(rename = "kling_2.1_pro")]
Kling21Pro,
#[serde(rename = "kling_2.1_master")]
Kling21Master,
#[serde(rename = "kling_2p5_turbo_pro")]
Kling2p5TurboPro,
#[serde(rename = "kling_2p6_pro")]
Kling2p6Pro,
#[serde(rename = "kling_3p0_standard")]
Kling3p0Standard,
#[serde(rename = "kling_3p0_pro")]
Kling3p0Pro,
#[serde(rename = "happy_horse_1p0")]
HappyHorse1p0,
#[serde(rename = "seedance_1.0_lite")]
Seedance10Lite,
#[serde(rename = "seedance_1p5_pro")]
Seedance1p5Pro,
#[serde(rename = "seedance_2p0")]
Seedance2p0,
#[serde(rename = "seedance_2p0_fast")]
Seedance2p0Fast,
#[serde(rename = "sora_2")]
Sora2,
#[serde(rename = "sora_2_pro")]
Sora2Pro,
#[serde(rename = "veo_2")]
Veo2,
#[serde(rename = "veo_3")]
Veo3,
#[serde(rename = "veo_3_fast")]
Veo3Fast,
#[serde(rename = "veo_3p1")]
Veo3p1,
#[serde(rename = "veo_3p1_fast")]
Veo3p1Fast,
}
#[derive(Deserialize, Debug, Clone)]
pub struct TauriGenerateVideoRequest {
pub provider: Option<GenerationProvider>,
pub model: Option<TauriVideoModel>,
pub prompt: Option<String>,
pub negative_prompt: Option<String>,
#[deprecated(note = "Use start_frame_image_media_token instead")]
pub image_media_token: Option<MediaFileToken>,
pub start_frame_image_media_token: Option<MediaFileToken>,
pub end_frame_image_media_token: Option<MediaFileToken>,
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
pub reference_video_media_tokens: Option<Vec<MediaFileToken>>,
pub reference_audio_media_tokens: Option<Vec<MediaFileToken>>,
pub reference_character_tokens: Option<Vec<CharacterToken>>,
pub aspect_ratio: Option<CommonAspectRatio>,
pub resolution: Option<CommonResolution>,
pub duration_seconds: Option<u16>,
pub generate_audio: Option<bool>,
pub video_batch_count: Option<u16>,
#[deprecated(note = "Use start_frame_image_media_token instead")]
pub sora_orientation: Option<SoraOrientation>,
#[deprecated(note = "Use start_frame_image_media_token instead")]
pub grok_aspect_ratio: Option<GrokAspectRatio>,
pub frontend_caller: Option<TauriCommandCaller>,
pub frontend_subscriber_id: Option<String>,
pub frontend_subscriber_payload: Option<String>,
}
#[derive(Deserialize, Debug, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum SoraOrientation {
Portrait,
Landscape,
}
#[derive(Deserialize, Debug, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum GrokAspectRatio {
Portrait,
Landscape,
Square,
}
#[derive(Serialize)]
pub struct TauriGenerateVideoResponse {
}
impl SerializeMarker for TauriGenerateVideoResponse {}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum TauriGenerateVideoErrorType {
ModelNotSpecified,
NoProviderAvailable,
ServerError,
NeedsFalApiKey,
FalError,
NeedsStorytellerCredentials,
}
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::core::events::generation_events::common::GenerationModel;
@@ -19,7 +19,7 @@ use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub async fn handle_sora_sora2(
request: &EnqueueImageToVideoRequest,
request: &TauriGenerateVideoRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,

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