diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/cost_estimate/image/estimate_image_cost.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/cost_estimate/image/estimate_image_cost.rs new file mode 100644 index 0000000000..d3cc992cf2 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/cost_estimate/image/estimate_image_cost.rs @@ -0,0 +1,20 @@ +use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; +use crate::error::storyteller_error::StorytellerError; +use crate::utils::api_host::ApiHost; +use crate::utils::basic_json_post_request::basic_json_post_request; +use artcraft_api_defs::generate::cost_estimate::estimate_image_cost::{ + EstimateImageCostRequest, EstimateImageCostResponse, ESTIMATE_IMAGE_COST_PATH, +}; + +pub async fn estimate_image_cost( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + request: EstimateImageCostRequest, +) -> Result { + Ok(basic_json_post_request( + api_host, + ESTIMATE_IMAGE_COST_PATH, + maybe_creds, + request, + ).await?) +} diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/cost_estimate/image/mod.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/cost_estimate/image/mod.rs new file mode 100644 index 0000000000..fc1b4c41f0 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/cost_estimate/image/mod.rs @@ -0,0 +1 @@ +pub mod estimate_image_cost; diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/cost_estimate/mod.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/cost_estimate/mod.rs index b04b953edb..c10a6ca15e 100644 --- a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/cost_estimate/mod.rs +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/cost_estimate/mod.rs @@ -1 +1,2 @@ +pub mod image; pub mod video; diff --git a/crates/api_clients/artcraft/artcraft_client/src/utils/basic_json_post_request.rs b/crates/api_clients/artcraft/artcraft_client/src/utils/basic_json_post_request.rs index 2969da3e5f..b430a7a532 100644 --- a/crates/api_clients/artcraft/artcraft_client/src/utils/basic_json_post_request.rs +++ b/crates/api_clients/artcraft/artcraft_client/src/utils/basic_json_post_request.rs @@ -19,7 +19,16 @@ pub async fn basic_json_post_request( let url = get_route(api_host, route_path); - info!("Requesting {:?}", &url); + // NB: We record lightweight analytics telemetry to understand which areas + // of the tool are useful and which models and services are important. + let debug_log = url.ends_with("/v1/analytics/active_user_v2"); + + // Suppress "spammy" logs at the debug level. + if debug_log { + debug!("Requesting {:?}", &url); + } else { + info!("Requesting {:?}", &url); + } let client = Client::builder() .gzip(true) diff --git a/crates/desktop/artcraft/src/core/commands/cost_estimate/estimate_image_cost_command.rs b/crates/desktop/artcraft/src/core/commands/cost_estimate/estimate_image_cost_command.rs new file mode 100644 index 0000000000..41fa6d1a55 --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/cost_estimate/estimate_image_cost_command.rs @@ -0,0 +1,42 @@ +use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus}; +use crate::core::commands::response::shorthand::ResponseOrError; +use crate::core::commands::response::success_response_wrapper::SerializeMarker; +use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; +use artcraft_api_defs::generate::cost_estimate::estimate_image_cost::{ + EstimateImageCostError, EstimateImageCostErrorType, EstimateImageCostRequest, + EstimateImageCostResponse, +}; +use log::info; +use artcraft_client::endpoints::generate::cost_estimate::image::estimate_image_cost::estimate_image_cost; +use tauri::State; + +impl SerializeMarker for EstimateImageCostResponse {} + +#[tauri::command] +pub async fn estimate_image_cost_command( + request: EstimateImageCostRequest, + app_env_configs: State<'_, AppEnvConfigs>, +) -> ResponseOrError { + info!("estimate_image_cost_command called"); + + let result = estimate_image_cost( + &app_env_configs.storyteller_host, + None, // Credentials are not required for this endpoint. + request, + ) + .await; + + match result { + Ok(response) => Ok(response.into()), + Err(err) => Err(CommandErrorResponseWrapper { + status: CommandErrorStatus::BadRequest, + error_message: None, + error_type: None, + error_details: Some(EstimateImageCostError { + success: false, + error_type: EstimateImageCostErrorType::InvalidInput, + error_message: err.to_string(), + }), + }), + } +} diff --git a/crates/desktop/artcraft/src/core/commands/cost_estimate/mod.rs b/crates/desktop/artcraft/src/core/commands/cost_estimate/mod.rs index 2bc6984e30..6a7b22be0d 100644 --- a/crates/desktop/artcraft/src/core/commands/cost_estimate/mod.rs +++ b/crates/desktop/artcraft/src/core/commands/cost_estimate/mod.rs @@ -1 +1,2 @@ +pub mod estimate_image_cost_command; pub mod estimate_video_cost_command; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_artcraft_nano_banana_pro_edit.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_artcraft_nano_banana_pro_edit.rs deleted file mode 100644 index 4f9a6ade71..0000000000 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_artcraft_nano_banana_pro_edit.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_nano_banana_pro::aspect_ratio_to_artcraft_nano_banana_pro; -use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError}; -use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::ImageEditModel::NanoBanana; -use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageResolution, EditImageSize, EnqueueEditImageCommand}; -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::events::basic_sendable_event_trait::BasicSendableEvent; -use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider}; -use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent; -use crate::core::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::sora::state::sora_credential_manager::SoraCredentialManager; -use crate::services::sora::state::sora_task_queue::SoraTaskQueue; -use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; -use anyhow::anyhow; -use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{FluxProKontextMaxEditImageNumImages, FluxProKontextMaxEditImageRequest}; -use artcraft_api_defs::generate::image::edit::gemini_25_flash_edit_image::{Gemini25FlashEditImageNumImages, Gemini25FlashEditImageRequest}; -use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest}; -use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{NanoBananaProMultiFunctionImageGenAspectRatio, NanoBananaProMultiFunctionImageGenImageResolution, NanoBananaProMultiFunctionImageGenNumImages, NanoBananaProMultiFunctionImageGenRequest}; -use enums::common::generation_provider::GenerationProvider; -use enums::tauri::tasks::task_type::TaskType; -use idempotency::uuid::generate_random_uuid; -use log::{error, info}; -use artcraft_client::endpoints::generate::image::edit::flux_pro_kontext_max_edit_image::flux_pro_kontext_max_edit_image; -use artcraft_client::endpoints::generate::image::edit::gemini_25_flash_edit_image::gemini_25_flash_edit_image; -use artcraft_client::endpoints::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image; -use artcraft_client::endpoints::generate::image::multi_function::nano_banana_pro_multi_function_image_gen_image::nano_banana_pro_multi_function_image_gen; -use tauri::AppHandle; - -pub(super) const MAX_IMAGES: usize = 10; - -pub async fn handle_artcraft_nano_banana_pro_edit( - request: &EnqueueEditImageCommand, - app: &AppHandle, - app_data_root: &AppDataRoot, - app_env_configs: &AppEnvConfigs, - storyteller_creds_manager: &StorytellerCredentialManager, -) -> Result { - - let creds = match storyteller_creds_manager.get_credentials()? { - Some(creds) => creds, - None => { - return Err(GenerateError::needs_storyteller_credentials()); - }, - }; - - info!("Calling Artcraft Nano Banana (edit) ..."); - - let uuid_idempotency_token = generate_random_uuid(); - - let num_images = match request.image_count { - None => None, - Some(1) => Some(NanoBananaProMultiFunctionImageGenNumImages::One), - Some(2) => Some(NanoBananaProMultiFunctionImageGenNumImages::Two), - Some(3) => Some(NanoBananaProMultiFunctionImageGenNumImages::Three), - Some(4) => Some(NanoBananaProMultiFunctionImageGenNumImages::Four), - Some(other) => { - return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages { - min: 1, - max: 4, - requested: other, - })); - }, - }; - - let aspect_ratio = get_aspect_ratio(request); - - let resolution = request.image_resolution - .map(|image_resolution| match image_resolution { - EditImageResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK, - EditImageResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK, - EditImageResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK, - }); - - let mut media_tokens = Vec::with_capacity(10); - - if let Some(scene_image_media_token) = request.scene_image_media_token.clone() { - media_tokens.push(scene_image_media_token); - } - - if let Some(image_media_tokens) = request.image_media_tokens.as_ref() { - media_tokens.extend_from_slice(image_media_tokens); - } - - if media_tokens.len() > MAX_IMAGES { - return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages { - min: 1, - max: MAX_IMAGES as u32, - provided: media_tokens.len() as u32, - })); - } - - let request = NanoBananaProMultiFunctionImageGenRequest { - uuid_idempotency_token, - prompt: Some(request.prompt.clone()), - image_media_tokens: Some(media_tokens), - num_images, - resolution, - aspect_ratio, - }; - - let result = nano_banana_pro_multi_function_image_gen( - &app_env_configs.storyteller_host, - Some(&creds), - request, - ).await; - - let job_id = match result { - Ok(enqueued) => { - // TODO(bt,2025-07-05): Enqueue job token? - info!("Successfully enqueued Artcraft Nano Banana. Job token: {}", - enqueued.inference_job_token); - enqueued.inference_job_token - } - Err(err) => { - error!("Failed to use Artcraft Nano Banana: {:?}", err); - return Err(GenerateError::from(err)); - } - }; - - Ok(TaskEnqueueSuccess { - provider: GenerationProvider::Artcraft, - model: Some(GenerationModel::NanoBananaPro), - provider_job_id: Some(job_id.to_string()), - task_type: TaskType::ImageGeneration, - }) -} - -fn get_aspect_ratio(request: &EnqueueEditImageCommand) -> Option { - if let Some(common_aspect_ratio) = request.common_aspect_ratio { - // Handle modern aspect ratio - let aspect = aspect_ratio_to_artcraft_nano_banana_pro(common_aspect_ratio); - return Some(aspect); - } - - if let Some(aspect_ratio) = request.aspect_ratio { - // Handle deprecated aspect ratio - return match aspect_ratio { - EditImageSize::Auto => Some(NanoBananaProMultiFunctionImageGenAspectRatio::Auto), - EditImageSize::Square => Some(NanoBananaProMultiFunctionImageGenAspectRatio::OneByOne), - EditImageSize::Wide => Some(NanoBananaProMultiFunctionImageGenAspectRatio::SixteenByNine), - EditImageSize::Tall => Some(NanoBananaProMultiFunctionImageGenAspectRatio::NineBySixteen), - } - } - - None -} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_image_edit_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_image_edit_artcraft.rs index 51e07b0731..b96b25babc 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_image_edit_artcraft.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_image_edit_artcraft.rs @@ -3,14 +3,16 @@ use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_flux_k 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_artcraft_gpt_image_1p5_edit::handle_artcraft_gpt_image_1p5_edit; use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_nano_banana_edit::handle_artcraft_nano_banana_edit; -use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_nano_banana_pro_edit::handle_artcraft_nano_banana_pro_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::artcraft::handle_artcraft_seedream_4_edit::handle_artcraft_seedream_4_edit; use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_seedream_4p5_edit::handle_artcraft_seedream_4p5_edit; use crate::core::commands::enqueue::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; use crate::core::state::data_dir::app_data_root::AppDataRoot; use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; +use artcraft_router::api::common_image_model::CommonImageModel; use tauri::AppHandle; pub async fn handle_image_edit_artcraft( @@ -27,7 +29,7 @@ pub async fn handle_image_edit_artcraft( ImageEditModel::Gemini25Flash | ImageEditModel::NanoBanana => { handle_artcraft_nano_banana_edit( request, app, app_data_root, app_env_configs, storyteller_creds_manager).await }, - ImageEditModel::NanoBananaPro => handle_artcraft_nano_banana_pro_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await, + ImageEditModel::NanoBananaPro => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBananaPro, GenerationModel::NanoBananaPro).await, ImageEditModel::GptImage1 => handle_artcraft_gpt_image_1_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await, ImageEditModel::GptImage1p5 => handle_artcraft_gpt_image_1p5_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await, ImageEditModel::Seedream4 => handle_artcraft_seedream_4_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_image_edit_artcraft_via_router.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_image_edit_artcraft_via_router.rs new file mode 100644 index 0000000000..b1e7fc3b1f --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_image_edit_artcraft_via_router.rs @@ -0,0 +1,153 @@ +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::{ + EditImageResolution, EditImageSize, EnqueueEditImageCommand, +}; +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 crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; +use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterCommonAspectRatio; +use artcraft_router::api::common_image_model::CommonImageModel; +use artcraft_router::api::common_resolution::CommonResolution as RouterCommonResolution; +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 log::{error, info}; + +pub(super) const MAX_EDIT_IMAGES: usize = 10; + +pub(super) async fn handle_image_edit_artcraft_via_router( + request: &EnqueueEditImageCommand, + app_env_configs: &AppEnvConfigs, + storyteller_creds_manager: &StorytellerCredentialManager, + model: CommonImageModel, + generation_model: GenerationModel, +) -> Result { + let creds = match storyteller_creds_manager.get_credentials()? { + Some(creds) => creds, + None => return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials)), + }; + + let client = RouterClient::Artcraft(RouterArtcraftClient::new( + app_env_configs.storyteller_host.clone(), + creds, + )); + + let mut media_tokens = Vec::with_capacity(MAX_EDIT_IMAGES); + + if let Some(scene_token) = request.scene_image_media_token.clone() { + media_tokens.push(scene_token); + } + if let Some(tokens) = request.image_media_tokens.as_ref() { + media_tokens.extend_from_slice(tokens); + } + + if media_tokens.len() > MAX_EDIT_IMAGES { + return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages { + min: 1, + max: MAX_EDIT_IMAGES as u32, + provided: media_tokens.len() as u32, + })); + } + + let image_inputs = if media_tokens.is_empty() { + None + } else { + Some(ImageListRef::MediaFileTokens(&media_tokens)) + }; + + let aspect_ratio = get_aspect_ratio_edit(request); + let resolution = get_resolution_edit(request); + + let router_request = GenerateImageRequest { + model, + provider: Provider::Artcraft, + prompt: Some(request.prompt.as_str()), + image_inputs, + resolution, + aspect_ratio, + image_batch_count: request.image_count.map(|n| n as u16), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, + idempotency_token: None, + }; + + let plan = router_request.build()?; + + info!("Image Generation Plan: {:?}", plan); + + let response = match plan.generate_image(&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)?; + + info!("Job token: {}", job_id); + + Ok(TaskEnqueueSuccess { + task_type: TaskType::ImageGeneration, + model: Some(generation_model), + provider: GenerationProvider::Artcraft, + provider_job_id: Some(job_id), + }) +} + +fn get_aspect_ratio_edit(request: &EnqueueEditImageCommand) -> Option { + if let Some(ar) = request.common_aspect_ratio { + return Some(convert_desktop_aspect_ratio(ar)); + } + if let Some(ar) = request.aspect_ratio { + return Some(match ar { + EditImageSize::Auto => RouterCommonAspectRatio::Auto, + EditImageSize::Square => RouterCommonAspectRatio::Square, + EditImageSize::Wide => RouterCommonAspectRatio::Wide, + EditImageSize::Tall => RouterCommonAspectRatio::Tall, + }); + } + None +} + +fn get_resolution_edit(request: &EnqueueEditImageCommand) -> Option { + request.image_resolution.map(|res| match res { + EditImageResolution::OneK => RouterCommonResolution::OneK, + EditImageResolution::TwoK => RouterCommonResolution::TwoK, + EditImageResolution::FourK => RouterCommonResolution::FourK, + }) +} + +fn convert_desktop_aspect_ratio(ar: DesktopCommonAspectRatio) -> RouterCommonAspectRatio { + match ar { + DesktopCommonAspectRatio::Auto => RouterCommonAspectRatio::Auto, + DesktopCommonAspectRatio::Square => RouterCommonAspectRatio::Square, + DesktopCommonAspectRatio::WideThreeByTwo => RouterCommonAspectRatio::WideThreeByTwo, + DesktopCommonAspectRatio::WideFourByThree => RouterCommonAspectRatio::WideFourByThree, + DesktopCommonAspectRatio::WideFiveByFour => RouterCommonAspectRatio::WideFiveByFour, + DesktopCommonAspectRatio::WideSixteenByNine => RouterCommonAspectRatio::WideSixteenByNine, + DesktopCommonAspectRatio::WideTwentyOneByNine => RouterCommonAspectRatio::WideTwentyOneByNine, + DesktopCommonAspectRatio::TallTwoByThree => RouterCommonAspectRatio::TallTwoByThree, + DesktopCommonAspectRatio::TallThreeByFour => RouterCommonAspectRatio::TallThreeByFour, + DesktopCommonAspectRatio::TallFourByFive => RouterCommonAspectRatio::TallFourByFive, + DesktopCommonAspectRatio::TallNineBySixteen => RouterCommonAspectRatio::TallNineBySixteen, + DesktopCommonAspectRatio::TallNineByTwentyOne => RouterCommonAspectRatio::TallNineByTwentyOne, + DesktopCommonAspectRatio::Wide => RouterCommonAspectRatio::Wide, + DesktopCommonAspectRatio::Tall => RouterCommonAspectRatio::Tall, + DesktopCommonAspectRatio::Auto2k => RouterCommonAspectRatio::Auto2k, + DesktopCommonAspectRatio::Auto4k => RouterCommonAspectRatio::Auto4k, + DesktopCommonAspectRatio::SquareHd => RouterCommonAspectRatio::SquareHd, + } +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/mod.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/mod.rs index cc0e60ba3f..0e671f222f 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/mod.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/mod.rs @@ -1,8 +1,8 @@ +mod handle_image_edit_artcraft_via_router; mod handle_artcraft_flux_kontext_edit; mod handle_artcraft_gpt_image_1_edit; mod handle_artcraft_gpt_image_1p5_edit; mod handle_artcraft_nano_banana_edit; -mod handle_artcraft_nano_banana_pro_edit; mod handle_artcraft_seedream_4_edit; mod handle_artcraft_seedream_4p5_edit; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_artcraft_nano_banana_pro_text_to_image.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_artcraft_nano_banana_pro_text_to_image.rs deleted file mode 100644 index 30a94ddc3e..0000000000 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_artcraft_nano_banana_pro_text_to_image.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio; -use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_nano_banana_pro::aspect_ratio_to_artcraft_nano_banana_pro; -use crate::core::api_adapters::resolution::common_resolution::CommonResolution; -use crate::core::api_adapters::resolution::convert::resolution_to_artcraft_nano_banana_pro::resolution_to_artcraft_nano_banana_pro; -use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason}; -use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageResolution, EditImageSize}; -use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; -use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageResolution, 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; -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::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_api_defs::generate::image::edit::gemini_25_flash_edit_image::{Gemini25FlashEditImageNumImages, Gemini25FlashEditImageRequest}; -use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{NanoBananaProMultiFunctionImageGenAspectRatio, NanoBananaProMultiFunctionImageGenImageResolution, NanoBananaProMultiFunctionImageGenNumImages, NanoBananaProMultiFunctionImageGenRequest}; -use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest}; -use enums::common::generation_provider::GenerationProvider; -use enums::tauri::tasks::task_type::TaskType; -use idempotency::uuid::generate_random_uuid; -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 artcraft_client::endpoints::generate::image::edit::gemini_25_flash_edit_image::gemini_25_flash_edit_image; -use artcraft_client::endpoints::generate::image::multi_function::nano_banana_pro_multi_function_image_gen_image::nano_banana_pro_multi_function_image_gen; -use tauri::AppHandle; - -pub async fn handle_artcraft_nano_banana_pro_text_to_image( - request: &EnqueueTextToImageRequest, - app_env_configs: &AppEnvConfigs, - storyteller_creds_manager: &StorytellerCredentialManager, -) -> Result { - - let creds = match storyteller_creds_manager.get_credentials()? { - Some(creds) => creds, - None => { - return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials)); - }, - }; - - info!("Calling Artcraft Nano Banana Pro..."); - - let uuid_idempotency_token = generate_random_uuid(); - - let num_images = match request.number_images { - None => None, - Some(1) => Some(NanoBananaProMultiFunctionImageGenNumImages::One), - Some(2) => Some(NanoBananaProMultiFunctionImageGenNumImages::Two), - Some(3) => Some(NanoBananaProMultiFunctionImageGenNumImages::Three), - Some(4) => Some(NanoBananaProMultiFunctionImageGenNumImages::Four), - Some(other) => { - return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages { - min: 1, - max: 4, - requested: other, - })); - }, - }; - - let aspect_ratio = get_aspect_ratio(request); - let resolution = get_resolution(request); - - let request = NanoBananaProMultiFunctionImageGenRequest { - uuid_idempotency_token, - prompt: request.prompt.clone(), - image_media_tokens: request.image_media_tokens.clone(), - num_images, - resolution, - aspect_ratio, - }; - - info!("Calling Artcraft with request {:?}", request); - - let result = nano_banana_pro_multi_function_image_gen( - &app_env_configs.storyteller_host, - Some(&creds), - request, - ).await; - - let job_id = match result { - Ok(enqueued) => { - // TODO(bt,2025-07-05): Enqueue job token? - info!("Successfully enqueued Artcraft Nano Banana. Job token: {}", - enqueued.inference_job_token); - enqueued.inference_job_token - } - Err(err) => { - error!("Failed to use Artcraft Nano Banana: {:?}", err); - return Err(GenerateError::ProviderFailure(ProviderFailureReason::StorytellerError(err))); - } - }; - - Ok(TaskEnqueueSuccess { - provider: GenerationProvider::Artcraft, - model: Some(GenerationModel::NanoBananaPro), - provider_job_id: Some(job_id.to_string()), - task_type: TaskType::ImageGeneration, - }) -} - -fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option { - if let Some(common_aspect_ratio) = request.common_aspect_ratio { - // Handle modern aspect ratio - let aspect = aspect_ratio_to_artcraft_nano_banana_pro(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(NanoBananaProMultiFunctionImageGenAspectRatio::Auto), - TextToImageSize::Square => Some(NanoBananaProMultiFunctionImageGenAspectRatio::OneByOne), - TextToImageSize::Wide => Some(NanoBananaProMultiFunctionImageGenAspectRatio::SixteenByNine), - TextToImageSize::Tall => Some(NanoBananaProMultiFunctionImageGenAspectRatio::NineBySixteen), - } - } - - None -} - -fn get_resolution(request: &EnqueueTextToImageRequest) -> Option { - if let Some(common_resolution) = request.common_resolution { - return Some(resolution_to_artcraft_nano_banana_pro(common_resolution)); - } - - if let Some(resolution) = request.image_resolution { - // Deprecated resolution - return Some(match resolution { - TextToImageResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK, - TextToImageResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK, - TextToImageResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK, - }) - } - - None -} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_text_to_image_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_text_to_image_artcraft.rs index a34f0e4816..ed9fe4fc26 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_text_to_image_artcraft.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_text_to_image_artcraft.rs @@ -6,15 +6,17 @@ use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_flu use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_flux_pro_1p1_ultra_text_to_image::handle_artcraft_flux_pro_1p1_ultra_text_to_image; 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_artcraft_gpt_image_1p5_text_to_image::handle_artcraft_gpt_image_1p5_text_to_image; -use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_nano_banana_pro_text_to_image::handle_artcraft_nano_banana_pro_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::artcraft::handle_artcraft_nano_banana_text_to_image::handle_artcraft_nano_banana_text_to_image; use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_seedream_4_text_to_image::handle_artcraft_seedream_4_text_to_image; use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_seedream_4p5_text_to_image::handle_artcraft_seedream_4p5_text_to_image; 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::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; use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; +use artcraft_router::api::common_image_model::CommonImageModel; use enums::common::generation_provider::GenerationProvider; use tauri::AppHandle; @@ -46,7 +48,7 @@ pub async fn handle_text_to_image_artcraft( TextToImageModel::Gemini25Flash | TextToImageModel::NanoBanana => { handle_artcraft_nano_banana_text_to_image(request, app_env_configs, storyteller_creds_manager).await }, - TextToImageModel::NanoBananaPro => handle_artcraft_nano_banana_pro_text_to_image(request, app_env_configs, storyteller_creds_manager).await, + TextToImageModel::NanoBananaPro => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBananaPro, GenerationModel::NanoBananaPro).await, TextToImageModel::Seedream4 => handle_artcraft_seedream_4_text_to_image(request, app_env_configs, storyteller_creds_manager).await, TextToImageModel::Seedream4p5 => handle_artcraft_seedream_4p5_text_to_image(request, app_env_configs, storyteller_creds_manager).await, } diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_text_to_image_artcraft_via_router.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_text_to_image_artcraft_via_router.rs new file mode 100644 index 0000000000..bc27b82242 --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_text_to_image_artcraft_via_router.rs @@ -0,0 +1,144 @@ +use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio as CommonAspectRatio2; +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::{ + EnqueueTextToImageRequest, TextToImageResolution, TextToImageSize, +}; +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 artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterCommonAspectRatio; +use artcraft_router::api::common_image_model::CommonImageModel; +use artcraft_router::api::common_resolution::CommonResolution as RouterCommonResolution; +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 log::{error, info}; + +pub(super) async fn handle_text_to_image_artcraft_via_router( + request: &EnqueueTextToImageRequest, + app_env_configs: &AppEnvConfigs, + storyteller_creds_manager: &StorytellerCredentialManager, + model: CommonImageModel, + generation_model: GenerationModel, +) -> Result { + let creds = match storyteller_creds_manager.get_credentials()? { + Some(creds) => creds, + None => return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials)), + }; + + let client = RouterClient::Artcraft(RouterArtcraftClient::new( + app_env_configs.storyteller_host.clone(), + creds, + )); + + let image_inputs = request.image_media_tokens.as_ref().map(ImageListRef::MediaFileTokens); + + let aspect_ratio = get_aspect_ratio_t2i(request); + let resolution = get_resolution_t2i(request); + + let router_request = GenerateImageRequest { + model, + provider: Provider::Artcraft, + prompt: request.prompt.as_deref(), + image_inputs, + resolution, + aspect_ratio, + image_batch_count: request.number_images.map(|n| n as u16), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, + idempotency_token: None, + }; + + let plan = router_request.build()?; + + info!("Image Generation Plan: {:?}", plan); + + let response = match plan.generate_image(&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::ImageGeneration, + model: Some(generation_model), + provider: GenerationProvider::Artcraft, + provider_job_id: Some(job_id), + }) +} + +fn get_aspect_ratio_t2i(request: &EnqueueTextToImageRequest) -> Option { + if let Some(ar) = request.common_aspect_ratio { + return Some(convert_desktop_aspect_ratio(ar)); + } + if let Some(ar) = request.aspect_ratio { + return Some(match ar { + TextToImageSize::Auto => RouterCommonAspectRatio::Auto, + TextToImageSize::Square => RouterCommonAspectRatio::Square, + TextToImageSize::Wide => RouterCommonAspectRatio::Wide, + TextToImageSize::Tall => RouterCommonAspectRatio::Tall, + }); + } + None +} + +fn get_resolution_t2i(request: &EnqueueTextToImageRequest) -> Option { + if let Some(res) = request.common_resolution { + return Some(convert_desktop_resolution(res)); + } + if let Some(res) = request.image_resolution { + return Some(match res { + TextToImageResolution::OneK => RouterCommonResolution::OneK, + TextToImageResolution::TwoK => RouterCommonResolution::TwoK, + TextToImageResolution::FourK => RouterCommonResolution::FourK, + }); + } + None +} + +fn convert_desktop_aspect_ratio(ar: CommonAspectRatio2) -> RouterCommonAspectRatio { + match ar { + CommonAspectRatio2::Auto => RouterCommonAspectRatio::Auto, + CommonAspectRatio2::Square => RouterCommonAspectRatio::Square, + CommonAspectRatio2::WideThreeByTwo => RouterCommonAspectRatio::WideThreeByTwo, + CommonAspectRatio2::WideFourByThree => RouterCommonAspectRatio::WideFourByThree, + CommonAspectRatio2::WideFiveByFour => RouterCommonAspectRatio::WideFiveByFour, + CommonAspectRatio2::WideSixteenByNine => RouterCommonAspectRatio::WideSixteenByNine, + CommonAspectRatio2::WideTwentyOneByNine => RouterCommonAspectRatio::WideTwentyOneByNine, + CommonAspectRatio2::TallTwoByThree => RouterCommonAspectRatio::TallTwoByThree, + CommonAspectRatio2::TallThreeByFour => RouterCommonAspectRatio::TallThreeByFour, + CommonAspectRatio2::TallFourByFive => RouterCommonAspectRatio::TallFourByFive, + CommonAspectRatio2::TallNineBySixteen => RouterCommonAspectRatio::TallNineBySixteen, + CommonAspectRatio2::TallNineByTwentyOne => RouterCommonAspectRatio::TallNineByTwentyOne, + CommonAspectRatio2::Wide => RouterCommonAspectRatio::Wide, + CommonAspectRatio2::Tall => RouterCommonAspectRatio::Tall, + CommonAspectRatio2::Auto2k => RouterCommonAspectRatio::Auto2k, + CommonAspectRatio2::Auto4k => RouterCommonAspectRatio::Auto4k, + CommonAspectRatio2::SquareHd => RouterCommonAspectRatio::SquareHd, + } +} + +fn convert_desktop_resolution(res: CommonResolution2) -> RouterCommonResolution { + match res { + CommonResolution2::OneK => RouterCommonResolution::OneK, + CommonResolution2::TwoK => RouterCommonResolution::TwoK, + CommonResolution2::ThreeK => RouterCommonResolution::ThreeK, + CommonResolution2::FourK => RouterCommonResolution::FourK, + } +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/mod.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/mod.rs index 3032eaca41..626bc2f4c1 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/mod.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/mod.rs @@ -1,8 +1,8 @@ pub (super) mod handle_text_to_image_artcraft; +mod handle_text_to_image_artcraft_via_router; mod handle_artcraft_gpt_image_1_text_to_image; mod handle_artcraft_gpt_image_1p5_text_to_image; -mod handle_artcraft_nano_banana_pro_text_to_image; mod handle_artcraft_nano_banana_text_to_image; mod handle_artcraft_seedream_4_text_to_image; mod handle_artcraft_seedream_4p5_text_to_image; diff --git a/crates/desktop/artcraft/src/core/commands/task_queue/get_task_queue_command.rs b/crates/desktop/artcraft/src/core/commands/task_queue/get_task_queue_command.rs index 906767cd06..6d2e388add 100644 --- a/crates/desktop/artcraft/src/core/commands/task_queue/get_task_queue_command.rs +++ b/crates/desktop/artcraft/src/core/commands/task_queue/get_task_queue_command.rs @@ -13,7 +13,7 @@ use enums::tauri::tasks::task_model_type::TaskModelType; use enums::tauri::tasks::task_status::TaskStatus; use enums::tauri::tasks::task_type::TaskType; use errors::AnyhowResult; -use log::{error, info, warn}; +use log::{debug, error, info, warn}; use serde_derive::{Deserialize, Serialize}; use sqlite_tasks::queries::list_tasks_for_frontend::list_tasks_for_frontend; use artcraft_client::endpoints::media_files::delete_media_file::delete_media_file; @@ -72,7 +72,8 @@ pub async fn get_task_queue_command( task_database: State<'_, TaskDatabase>, ) -> ResponseOrErrorMessage { - info!("get_task_queue_command called"); + // NB: This is debug because it spams the logs. + debug!("get_task_queue_command called"); let result = handle_request( &task_database, diff --git a/crates/desktop/artcraft/src/lib.rs b/crates/desktop/artcraft/src/lib.rs index 5072e1994d..c641e25db2 100644 --- a/crates/desktop/artcraft/src/lib.rs +++ b/crates/desktop/artcraft/src/lib.rs @@ -6,6 +6,7 @@ use tauri::Manager; use crate::core::commands::app_preferences::get_app_preferences_command::get_app_preferences_command; use crate::core::commands::app_preferences::update_app_preference_command::update_app_preferences_command; +use crate::core::commands::cost_estimate::estimate_image_cost_command::estimate_image_cost_command; use crate::core::commands::cost_estimate::estimate_video_cost_command::estimate_video_cost_command; use crate::core::commands::download::download_directory_reveal_command::download_directory_reveal_command; use crate::core::commands::download::download_media_file_command::download_media_file_command; @@ -212,6 +213,7 @@ pub fn run() { enqueue_image_to_gaussian_command, enqueue_image_to_video_command, enqueue_text_to_image_command, + estimate_image_cost_command, estimate_video_cost_command, flip_image, get_app_info_command,