diff --git a/crates/api_clients/artcraft/artcraft_router/src/api/common_image_model.rs b/crates/api_clients/artcraft/artcraft_router/src/api/common_image_model.rs index f2f195b68d..86a1ae0513 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/api/common_image_model.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/api/common_image_model.rs @@ -5,6 +5,8 @@ use serde_derive::{Deserialize, Serialize}; #[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum CommonImageModel { + GptImage1p5, + NanaBanana, #[serde(rename = "nano_banana_pro")] NanaBananaPro, Seedream4, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_gpt_image_1p5.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_gpt_image_1p5.rs new file mode 100644 index 0000000000..384f974b4b --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_gpt_image_1p5.rs @@ -0,0 +1,119 @@ +use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{ + GptImage1p5MultiFunctionImageGenNumImages, GptImage1p5MultiFunctionImageGenQuality, + GptImage1p5MultiFunctionImageGenSize, +}; + +use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate; +use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::PlanArtcraftGptImage1p5; + +pub(crate) fn estimate_image_cost_artcraft_gpt_image_1p5( + plan: &PlanArtcraftGptImage1p5<'_>, +) -> ImageGenerationCostEstimate { + // Pricing varies by quality and image size. 1 credit = 1 USD cent. + // Low: $0.01/image (any size) + // Medium: $0.03/image (square/unset), $0.05/image (wide or tall) + // High: $0.13/image (square/unset), $0.20/image (wide or tall) + // + // From Fal (text to image) - + // + // Your request will cost different amounts based on the number of images, quality, and size. + // + // You will be charged $0.005 per 1,000 input text tokens. One word is roughly 4 tokens. + // You will be charged $0.010 per 1,000 output text tokens. The model will consume tokens reasoning about your prompt based on it's complexity. + // For low quality, you will be charged $0.009 for 1024x1024 or $0.013 for any other size per image. + // For medium quality, you will be charged $0.034 for 1024x1024, $0.051 for 1024x1536 and $0.050 for 1536x1024 per image. + // For high quality, you will be charged $0.133 for 1024x1024, $0.200 for 1024x1536 or $0.199 for 1536x1024 per image. + // + // From Fal (image to image) - + // + // Your request will cost different amounts based on the number of images, quality, and size. + // + // You will be charged $0.005 per 1,000 input text tokens. One word is roughly 4 tokens. + // You will be charged $0.008 per 1,000 input image tokens. One 1024x1024 image is roughly 135 tokens in low fidelity mode, or 3,050 tokens in high fidelity mode. + // You will be charged $0.010 per 1,000 output text tokens. The model will consume tokens reasoning about your prompt based on it's complexity. + // For low quality, you will be charged $0.009 for 1024x1024 or $0.013 for any other size per image. + // For medium quality, you will be charged $0.034 for 1024x1024, $0.051 for 1024x1536 and $0.050 for 1536x1024 per image. + // For high quality, you will be charged $0.133 for 1024x1024, $0.200 for 1024x1536 or $0.199 for 1536x1024 per image. + // + let cost_per_image: u64 = match plan.quality { + GptImage1p5MultiFunctionImageGenQuality::Low => 1, + GptImage1p5MultiFunctionImageGenQuality::Medium => match plan.image_size { + Some(GptImage1p5MultiFunctionImageGenSize::Square) | None => 3, + Some(GptImage1p5MultiFunctionImageGenSize::Wide) | Some(GptImage1p5MultiFunctionImageGenSize::Tall) => 5, + }, + GptImage1p5MultiFunctionImageGenQuality::High => match plan.image_size { + Some(GptImage1p5MultiFunctionImageGenSize::Square) | None => 13, + Some(GptImage1p5MultiFunctionImageGenSize::Wide) | Some(GptImage1p5MultiFunctionImageGenSize::Tall) => 20, + }, + }; + + let num_images: u64 = match plan.num_images { + GptImage1p5MultiFunctionImageGenNumImages::One => 1, + GptImage1p5MultiFunctionImageGenNumImages::Two => 2, + GptImage1p5MultiFunctionImageGenNumImages::Three => 3, + GptImage1p5MultiFunctionImageGenNumImages::Four => 4, + }; + + let cost_in_usd_cents = cost_per_image * num_images; + + ImageGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_aspect_ratio::CommonAspectRatio; + use crate::api::common_image_model::CommonImageModel; + use crate::api::provider::Provider; + use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; + use crate::generate::generate_image::generate_image_request::GenerateImageRequest; + + fn estimate_usd_cents(image_batch_count: u16, aspect_ratio: Option) -> u64 { + let request = GenerateImageRequest { + model: CommonImageModel::GptImage1p5, + provider: Provider::Artcraft, + prompt: None, + image_inputs: None, + resolution: None, + aspect_ratio, + image_batch_count: Some(image_batch_count), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + idempotency_token: None, + }; + request.build() + .expect("build should succeed") + .estimate_costs() + .cost_in_usd_cents + .expect("cost_in_usd_cents should be present") + } + + #[test] + fn test_estimate_cost_medium_square() { + // Default quality (Medium) + square/unset size = 3 cents each + assert_eq!(estimate_usd_cents(1, None), 3); + assert_eq!(estimate_usd_cents(1, Some(CommonAspectRatio::Square)), 3); + assert_eq!(estimate_usd_cents(2, Some(CommonAspectRatio::Square)), 6); + assert_eq!(estimate_usd_cents(3, Some(CommonAspectRatio::Square)), 9); + assert_eq!(estimate_usd_cents(4, Some(CommonAspectRatio::Square)), 12); + } + + #[test] + fn test_estimate_cost_medium_wide() { + // Medium + wide = 5 cents each + assert_eq!(estimate_usd_cents(1, Some(CommonAspectRatio::WideSixteenByNine)), 5); + assert_eq!(estimate_usd_cents(4, Some(CommonAspectRatio::WideSixteenByNine)), 20); + } + + #[test] + fn test_estimate_cost_medium_tall() { + // Medium + tall = 5 cents each + assert_eq!(estimate_usd_cents(1, Some(CommonAspectRatio::TallNineBySixteen)), 5); + assert_eq!(estimate_usd_cents(4, Some(CommonAspectRatio::TallNineBySixteen)), 20); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana.rs new file mode 100644 index 0000000000..743fd7f357 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana.rs @@ -0,0 +1,65 @@ +use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::NanoBananaMultiFunctionImageGenNumImages; + +use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate; +use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::PlanArtcraftNanaBanana; + +pub(crate) fn estimate_image_cost_artcraft_nano_banana( + plan: &PlanArtcraftNanaBanana<'_>, +) -> ImageGenerationCostEstimate { + // Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent. + let cost_per_image: u64 = 4; + + let num_images: u64 = match plan.num_images { + NanoBananaMultiFunctionImageGenNumImages::One => 1, + NanoBananaMultiFunctionImageGenNumImages::Two => 2, + NanoBananaMultiFunctionImageGenNumImages::Three => 3, + NanoBananaMultiFunctionImageGenNumImages::Four => 4, + }; + + let cost_in_usd_cents = cost_per_image * num_images; + + ImageGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_image_model::CommonImageModel; + use crate::api::provider::Provider; + use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; + use crate::generate::generate_image::generate_image_request::GenerateImageRequest; + + fn estimate_usd_cents(image_batch_count: u16) -> u64 { + let request = GenerateImageRequest { + model: CommonImageModel::NanaBanana, + provider: Provider::Artcraft, + prompt: None, + image_inputs: None, + resolution: None, + aspect_ratio: None, + image_batch_count: Some(image_batch_count), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + idempotency_token: None, + }; + request.build() + .expect("build should succeed") + .estimate_costs() + .cost_in_usd_cents + .expect("cost_in_usd_cents should be present") + } + + #[test] + fn test_estimate_cost_usd_cents() { + // $0.04/image = 4 cents each + assert_eq!(estimate_usd_cents(1), 4); + assert_eq!(estimate_usd_cents(2), 8); + assert_eq!(estimate_usd_cents(3), 12); + assert_eq!(estimate_usd_cents(4), 16); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/mod.rs index 4e1ccbc13f..fef19692cb 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/mod.rs @@ -1,3 +1,5 @@ +pub mod estimate_image_cost_artcraft_gpt_image_1p5; +pub mod estimate_image_cost_artcraft_nano_banana; pub mod estimate_image_cost_artcraft_nano_banana_pro; pub mod estimate_image_cost_artcraft_seedream_4; pub mod estimate_image_cost_artcraft_seedream_4p5; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_gpt_image_1p5.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_gpt_image_1p5.rs new file mode 100644 index 0000000000..3dc2ba18c5 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_gpt_image_1p5.rs @@ -0,0 +1,164 @@ +use crate::client::router_artcraft_client::RouterArtcraftClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_image::generate_image_response::{ + ArtcraftImageResponsePayload, GenerateImageResponse, +}; +use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::PlanArtcraftGptImage1p5; +use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::GptImage1p5MultiFunctionImageGenRequest; +use artcraft_client::endpoints::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen_image::gpt_image_1p5_multi_function_image_gen; + +pub async fn execute_artcraft_gpt_image_1p5( + plan: &PlanArtcraftGptImage1p5<'_>, + artcraft_client: &RouterArtcraftClient, +) -> Result { + let request = GptImage1p5MultiFunctionImageGenRequest { + uuid_idempotency_token: plan.idempotency_token.clone(), + prompt: plan.prompt.map(|p| p.to_string()), + image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()), + mask_image_token: None, + num_images: Some(plan.num_images), + image_size: plan.image_size, + background: None, + quality: Some(plan.quality), + input_fidelity: None, + output_format: None, + }; + + let response = gpt_image_1p5_multi_function_image_gen( + &artcraft_client.api_host, + Some(&artcraft_client.credentials), + request, + ) + .await + .map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?; + + Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload { + inference_job_token: response.inference_job_token, + })) +} + +#[cfg(test)] +mod tests { + use crate::api::common_aspect_ratio::CommonAspectRatio; + use crate::api::image_list_ref::ImageListRef; + use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; + use crate::generate::generate_image::generate_image_request::GenerateImageRequest; + use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan; + use crate::test_helpers::{base_gpt_image_1p5_image_request, get_artcraft_client}; + use strum::IntoEnumIterator; + use tokens::tokens::media_files::MediaFileToken; + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn test_text_to_image_gpt_image_1p5() { + let client = get_artcraft_client(); + let request = GenerateImageRequest { + aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + image_batch_count: Some(1), + prompt: Some("a horse walking through a cyberpunk city at night"), + ..base_gpt_image_1p5_image_request() + }; + + let plan = request.build().unwrap(); + let result = plan.generate_image(&client).await; + + println!("Result: {:?}", result); + let response = result.expect("generate_image request failed"); + let payload = response.get_artcraft_payload().expect("expected Artcraft payload"); + println!("Job token: {:?}", payload.inference_job_token); + + assert_eq!(1, 2); // NB: Intentional failure to inspect the response above. + } + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn test_edit_image_gpt_image_1p5() { + let client = get_artcraft_client(); + + let image_tokens = vec![ + MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"), + ]; + + let request = GenerateImageRequest { + prompt: Some("Change the background to a desert with cacti"), + image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)), + aspect_ratio: Some(CommonAspectRatio::Auto), + image_batch_count: Some(1), + ..base_gpt_image_1p5_image_request() + }; + + let plan = request.build().unwrap(); + let result = plan.generate_image(&client).await; + + println!("Result: {:?}", result); + let response = result.expect("generate_image request failed"); + let payload = response.get_artcraft_payload().expect("expected Artcraft payload"); + println!("Job token: {:?}", payload.inference_job_token); + + assert_eq!(1, 2); // NB: Intentional failure to inspect the response above. + } + + #[tokio::test] + #[ignore] // manually run — fires one real API request per aspect ratio variant (expensive) + async fn test_text_to_image_gpt_image_1p5_all_aspect_ratios() { + let client = get_artcraft_client(); + + for ar in CommonAspectRatio::iter() { + println!("--- text-to-image aspect ratio: {:?} ---", ar); + let request = GenerateImageRequest { + aspect_ratio: Some(ar), + image_batch_count: Some(1), + prompt: Some("a bear walking through a abandoned desert city at night"), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, + ..base_gpt_image_1p5_image_request() + }; + match request.build() { + Err(e) => println!(" plan error: {:?}", e), + Ok(plan) => match plan.generate_image(&client).await { + Ok(response) => { + let payload = response.get_artcraft_payload().expect("expected Artcraft payload"); + println!(" job token: {:?}", payload.inference_job_token); + } + Err(e) => println!(" generate error: {:?}", e), + }, + } + } + + assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above. + } + + #[tokio::test] + #[ignore] // manually run — fires one real API request per aspect ratio variant (expensive) + async fn test_edit_image_gpt_image_1p5_all_aspect_ratios() { + let client = get_artcraft_client(); + + let image_tokens = vec![ + MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"), + ]; + + for ar in CommonAspectRatio::iter() { + println!("--- image-edit aspect ratio: {:?} ---", ar); + let request = GenerateImageRequest { + prompt: Some("Change the background to an alien world"), + image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)), + aspect_ratio: Some(ar), + image_batch_count: Some(1), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, + ..base_gpt_image_1p5_image_request() + }; + match request.build() { + Err(e) => println!(" plan error: {:?}", e), + Ok(plan) => match plan.generate_image(&client).await { + Ok(response) => { + let payload = response.get_artcraft_payload().expect("expected Artcraft payload"); + println!(" job token: {:?}", payload.inference_job_token); + } + Err(e) => println!(" generate error: {:?}", e), + }, + } + } + + assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above. + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana.rs new file mode 100644 index 0000000000..4094986089 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana.rs @@ -0,0 +1,159 @@ +use crate::client::router_artcraft_client::RouterArtcraftClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_image::generate_image_response::{ + ArtcraftImageResponsePayload, GenerateImageResponse, +}; +use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::PlanArtcraftNanaBanana; +use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::NanoBananaMultiFunctionImageGenRequest; +use artcraft_client::endpoints::generate::image::multi_function::nano_banana_multi_function_image_gen_image::nano_banana_multi_function_image_gen; + +pub async fn execute_artcraft_nano_banana( + plan: &PlanArtcraftNanaBanana<'_>, + artcraft_client: &RouterArtcraftClient, +) -> Result { + let request = NanoBananaMultiFunctionImageGenRequest { + uuid_idempotency_token: plan.idempotency_token.clone(), + prompt: plan.prompt.map(|p| p.to_string()), + image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()), + num_images: Some(plan.num_images), + aspect_ratio: plan.aspect_ratio, + }; + + let response = nano_banana_multi_function_image_gen( + &artcraft_client.api_host, + Some(&artcraft_client.credentials), + request, + ) + .await + .map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?; + + Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload { + inference_job_token: response.inference_job_token, + })) +} + +#[cfg(test)] +mod tests { + use crate::api::common_aspect_ratio::CommonAspectRatio; + use crate::api::image_list_ref::ImageListRef; + use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; + use crate::generate::generate_image::generate_image_request::GenerateImageRequest; + use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan; + use crate::test_helpers::{base_nano_banana_image_request, get_artcraft_client}; + use strum::IntoEnumIterator; + use tokens::tokens::media_files::MediaFileToken; + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn test_text_to_image_nano_banana() { + let client = get_artcraft_client(); + let request = GenerateImageRequest { + aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + image_batch_count: Some(1), + prompt: Some("a cat walking through a cyberpunk city at night"), + ..base_nano_banana_image_request() + }; + + let plan = request.build().unwrap(); + let result = plan.generate_image(&client).await; + + println!("Result: {:?}", result); + let response = result.expect("generate_image request failed"); + let payload = response.get_artcraft_payload().expect("expected Artcraft payload"); + println!("Job token: {:?}", payload.inference_job_token); + + assert_eq!(1, 2); // NB: Intentional failure to inspect the response above. + } + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn test_edit_image_nano_banana() { + let client = get_artcraft_client(); + + let image_tokens = vec![ + MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"), + ]; + + let request = GenerateImageRequest { + prompt: Some("Change the background to a sunny beach"), + image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)), + aspect_ratio: Some(CommonAspectRatio::Auto), + image_batch_count: Some(1), + ..base_nano_banana_image_request() + }; + + let plan = request.build().unwrap(); + let result = plan.generate_image(&client).await; + + println!("Result: {:?}", result); + let response = result.expect("generate_image request failed"); + let payload = response.get_artcraft_payload().expect("expected Artcraft payload"); + println!("Job token: {:?}", payload.inference_job_token); + + assert_eq!(1, 2); // NB: Intentional failure to inspect the response above. + } + + #[tokio::test] + #[ignore] // manually run — fires one real API request per aspect ratio variant (expensive) + async fn test_text_to_image_nano_banana_all_aspect_ratios() { + let client = get_artcraft_client(); + + for ar in CommonAspectRatio::iter() { + println!("--- text-to-image aspect ratio: {:?} ---", ar); + let request = GenerateImageRequest { + aspect_ratio: Some(ar), + image_batch_count: Some(1), + prompt: Some("a shiba walking through a cyberpunk city at night"), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, + ..base_nano_banana_image_request() + }; + match request.build() { + Err(e) => println!(" plan error: {:?}", e), + Ok(plan) => match plan.generate_image(&client).await { + Ok(response) => { + let payload = response.get_artcraft_payload().expect("expected Artcraft payload"); + println!(" job token: {:?}", payload.inference_job_token); + } + Err(e) => println!(" generate error: {:?}", e), + }, + } + } + + assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above. + } + + #[tokio::test] + #[ignore] // manually run — fires one real API request per aspect ratio variant (expensive) + async fn test_edit_image_nano_banana_all_aspect_ratios() { + let client = get_artcraft_client(); + + let image_tokens = vec![ + MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"), + ]; + + for ar in CommonAspectRatio::iter() { + println!("--- image-edit aspect ratio: {:?} ---", ar); + let request = GenerateImageRequest { + prompt: Some("Change the background to a sunny beach"), + image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)), + aspect_ratio: Some(ar), + image_batch_count: Some(1), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, + ..base_nano_banana_image_request() + }; + match request.build() { + Err(e) => println!(" plan error: {:?}", e), + Ok(plan) => match plan.generate_image(&client).await { + Ok(response) => { + let payload = response.get_artcraft_payload().expect("expected Artcraft payload"); + println!(" job token: {:?}", payload.inference_job_token); + } + Err(e) => println!(" generate error: {:?}", e), + }, + } + } + + assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above. + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/mod.rs index d7c72dcddf..728520462b 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/mod.rs @@ -1,3 +1,5 @@ +pub mod generate_image_artcraft_gpt_image_1p5; +pub mod generate_image_artcraft_nano_banana; pub mod generate_image_artcraft_nano_banana_pro; pub mod generate_image_artcraft_seedream_4; pub mod generate_image_artcraft_seedream_4p5; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request.rs index 7c778bf1f3..1495db3ac9 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request.rs @@ -7,6 +7,8 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::errors::client_error::ClientError; use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan; +use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::plan_generate_image_artcraft_gpt_image_1p5; +use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::plan_generate_image_artcraft_nano_banana; use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::plan_generate_image_artcraft_nano_banana_pro; use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::plan_generate_image_artcraft_seedream_4; use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::plan_generate_image_artcraft_seedream_4p5; @@ -49,6 +51,12 @@ impl<'a> GenerateImageRequest<'a> { pub fn build(&self) -> Result, ArtcraftRouterError> { match self.provider { Provider::Artcraft => match self.model { + CommonImageModel::GptImage1p5 => { + plan_generate_image_artcraft_gpt_image_1p5(self).map(ImageGenerationPlan::ArtcraftGptImage1p5) + } + CommonImageModel::NanaBanana => { + plan_generate_image_artcraft_nano_banana(self).map(ImageGenerationPlan::ArtcraftNanaBanana) + } CommonImageModel::NanaBananaPro => { plan_generate_image_artcraft_nano_banana_pro(self).map(ImageGenerationPlan::ArtcraftNanaBananaPro) } @@ -63,10 +71,13 @@ impl<'a> GenerateImageRequest<'a> { CommonImageModel::NanaBananaPro => { plan_generate_image_fal_nano_banana_pro(self).map(ImageGenerationPlan::FalNanaBananaPro) } - CommonImageModel::Seedream4 | CommonImageModel::Seedream4p5 => { + CommonImageModel::GptImage1p5 + | CommonImageModel::NanaBanana + | CommonImageModel::Seedream4 + | CommonImageModel::Seedream4p5 => { Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "provider", - value: "Seedream is only available on the Artcraft provider".to_string(), + value: format!("{:?} is only available on the Artcraft provider", self.model), })) } }, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/image_generation_plan.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/image_generation_plan.rs index 77de99bbc5..a35371581b 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/image_generation_plan.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/image_generation_plan.rs @@ -1,15 +1,21 @@ use crate::client::router_client::RouterClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_gpt_image_1p5::estimate_image_cost_artcraft_gpt_image_1p5; +use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_nano_banana::estimate_image_cost_artcraft_nano_banana; use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_nano_banana_pro::estimate_image_cost_artcraft_nano_banana_pro; use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4::estimate_image_cost_artcraft_seedream_4; use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4p5::estimate_image_cost_artcraft_seedream_4p5; use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_pro::estimate_image_cost_fal_nano_banana_pro; +use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_gpt_image_1p5::execute_artcraft_gpt_image_1p5; +use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana::execute_artcraft_nano_banana; use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana_pro::execute_artcraft_nano_banana_pro; use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4::execute_artcraft_seedream_4; use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4p5::execute_artcraft_seedream_4p5; use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_pro::execute_fal_nano_banana_pro; use crate::generate::generate_image::generate_image_response::GenerateImageResponse; use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate; +use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::PlanArtcraftGptImage1p5; +use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::PlanArtcraftNanaBanana; use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::PlanArtcraftNanaBananaPro; use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::PlanArtcraftSeedream4; use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::PlanArtcraftSeedream4p5; @@ -17,6 +23,8 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_ban #[derive(Debug)] pub enum ImageGenerationPlan<'a> { + ArtcraftGptImage1p5(PlanArtcraftGptImage1p5<'a>), + ArtcraftNanaBanana(PlanArtcraftNanaBanana<'a>), ArtcraftNanaBananaPro(PlanArtcraftNanaBananaPro<'a>), ArtcraftSeedream4(PlanArtcraftSeedream4<'a>), ArtcraftSeedream4p5(PlanArtcraftSeedream4p5<'a>), @@ -29,6 +37,14 @@ impl<'a> ImageGenerationPlan<'a> { client: &RouterClient, ) -> Result { match self { + ImageGenerationPlan::ArtcraftGptImage1p5(plan) => { + let artcraft_client = client.get_artcraft_client_ref()?; + execute_artcraft_gpt_image_1p5(plan, artcraft_client).await + } + ImageGenerationPlan::ArtcraftNanaBanana(plan) => { + let artcraft_client = client.get_artcraft_client_ref()?; + execute_artcraft_nano_banana(plan, artcraft_client).await + } ImageGenerationPlan::ArtcraftNanaBananaPro(plan) => { let artcraft_client = client.get_artcraft_client_ref()?; execute_artcraft_nano_banana_pro(plan, artcraft_client).await @@ -50,6 +66,12 @@ impl<'a> ImageGenerationPlan<'a> { pub fn estimate_costs(&self) -> ImageGenerationCostEstimate { match self { + ImageGenerationPlan::ArtcraftGptImage1p5(plan) => { + estimate_image_cost_artcraft_gpt_image_1p5(plan) + } + ImageGenerationPlan::ArtcraftNanaBanana(plan) => { + estimate_image_cost_artcraft_nano_banana(plan) + } ImageGenerationPlan::ArtcraftNanaBananaPro(plan) => { estimate_image_cost_artcraft_nano_banana_pro(plan) } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/mod.rs index 161c96c8b1..9f2203b4a3 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/mod.rs @@ -1,3 +1,5 @@ +pub mod plan_generate_image_artcraft_gpt_image_1p5; +pub mod plan_generate_image_artcraft_nano_banana; pub mod plan_generate_image_artcraft_nano_banana_pro; pub mod plan_generate_image_artcraft_seedream_4; pub mod plan_generate_image_artcraft_seedream_4p5; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_gpt_image_1p5.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_gpt_image_1p5.rs new file mode 100644 index 0000000000..447c7e1b5a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_gpt_image_1p5.rs @@ -0,0 +1,310 @@ +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::image_list_ref::ImageListRef; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::client_error::ClientError; +use crate::generate::generate_image::generate_image_request::GenerateImageRequest; +use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{ + GptImage1p5MultiFunctionImageGenNumImages, GptImage1p5MultiFunctionImageGenQuality, + GptImage1p5MultiFunctionImageGenSize, +}; +use tokens::tokens::media_files::MediaFileToken; + +#[derive(Debug, Clone)] +pub struct PlanArtcraftGptImage1p5<'a> { + pub prompt: Option<&'a str>, + /// Input images for image editing. None means text-to-image mode. + pub image_inputs: Option<&'a Vec>, + pub image_size: Option, + /// Quality defaults to Medium when not specified in the request. + pub quality: GptImage1p5MultiFunctionImageGenQuality, + pub num_images: GptImage1p5MultiFunctionImageGenNumImages, + pub idempotency_token: String, +} + +pub fn plan_generate_image_artcraft_gpt_image_1p5<'a>( + request: &'a GenerateImageRequest<'a>, +) -> Result, ArtcraftRouterError> { + let strategy = request.request_mismatch_mitigation_strategy; + + let image_inputs = resolve_image_list_ref(request.image_inputs)?; + let image_size = plan_image_size(request.aspect_ratio, strategy)?; + let num_images = plan_num_images(request.image_batch_count, strategy)?; + + Ok(PlanArtcraftGptImage1p5 { + prompt: request.prompt, + image_inputs, + image_size, + quality: GptImage1p5MultiFunctionImageGenQuality::Medium, + num_images, + idempotency_token: request.get_or_generate_idempotency_token(), + }) +} + +fn resolve_image_list_ref<'a>( + image_list_ref: Option>, +) -> Result>, ArtcraftRouterError> { + match image_list_ref { + None => Ok(None), + Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)), + Some(ImageListRef::Urls(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens)) + } + } +} + +// GPT Image 1.5 image sizes: Square (1024x1024), Wide (1536x1024), Tall (1024x1536). +// All 17 CommonAspectRatio variants map to one of these three sizes or None. +// Auto aspect ratios yield None (let the API choose its default). +fn plan_image_size( + aspect_ratio: Option, + _strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use GptImage1p5MultiFunctionImageGenSize as GS; + match aspect_ratio { + // No preference or auto — let the model use its default + None + | Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(None), + + // Square + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(GS::Square)), + + // Wide + Some(CommonAspectRatio::WideThreeByTwo) + | Some(CommonAspectRatio::WideFourByThree) + | Some(CommonAspectRatio::WideFiveByFour) + | Some(CommonAspectRatio::WideSixteenByNine) + | Some(CommonAspectRatio::WideTwentyOneByNine) + | Some(CommonAspectRatio::Wide) => Ok(Some(GS::Wide)), + + // Tall + Some(CommonAspectRatio::TallTwoByThree) + | Some(CommonAspectRatio::TallThreeByFour) + | Some(CommonAspectRatio::TallFourByFive) + | Some(CommonAspectRatio::TallNineBySixteen) + | Some(CommonAspectRatio::TallNineByTwentyOne) + | Some(CommonAspectRatio::Tall) => Ok(Some(GS::Tall)), + } +} + +// GPT Image 1.5 supports 1, 2, 3, and 4 images. + +fn plan_num_images( + image_batch_count: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + use GptImage1p5MultiFunctionImageGenNumImages as GN; + let count = image_batch_count.unwrap_or(1); + match count { + 0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)), + 1 => Ok(GN::One), + 2 => Ok(GN::Two), + 3 => Ok(GN::Three), + 4 => Ok(GN::Four), + _ => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "image_batch_count", + value: format!("{}", count), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(GN::Four), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(GN::Four), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::common_aspect_ratio::CommonAspectRatio; + use crate::api::image_list_ref::ImageListRef; + use crate::errors::artcraft_router_error::ArtcraftRouterError; + use crate::errors::client_error::ClientError; + use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan; + use crate::test_helpers::base_gpt_image_1p5_image_request; + use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{ + GptImage1p5MultiFunctionImageGenNumImages as GN, + GptImage1p5MultiFunctionImageGenQuality, + GptImage1p5MultiFunctionImageGenSize as GS, + }; + + // ── Image size (aspect ratio mapping) ──────────────────────────────────── + + #[test] + fn image_size_none_is_none() { + let request = GenerateImageRequest { aspect_ratio: None, ..base_gpt_image_1p5_image_request() }; + let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") }; + assert!(plan.image_size.is_none()); + } + + #[test] + fn image_size_auto_variants_yield_none() { + for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] { + let request = GenerateImageRequest { aspect_ratio: Some(auto_ar), ..base_gpt_image_1p5_image_request() }; + let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") }; + assert!(plan.image_size.is_none(), "expected None for {:?}", auto_ar); + } + } + + #[test] + fn image_size_square_variants() { + for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] { + let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1p5_image_request() }; + let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") }; + assert!(matches!(plan.image_size, Some(GS::Square)), "expected Square for {:?}", ar); + } + } + + #[test] + fn image_size_wide_variants() { + let wide_ars = [ + CommonAspectRatio::WideThreeByTwo, + CommonAspectRatio::WideFourByThree, + CommonAspectRatio::WideFiveByFour, + CommonAspectRatio::WideSixteenByNine, + CommonAspectRatio::WideTwentyOneByNine, + CommonAspectRatio::Wide, + ]; + for ar in wide_ars { + let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1p5_image_request() }; + let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") }; + assert!(matches!(plan.image_size, Some(GS::Wide)), "expected Wide for {:?}", ar); + } + } + + #[test] + fn image_size_tall_variants() { + let tall_ars = [ + CommonAspectRatio::TallTwoByThree, + CommonAspectRatio::TallThreeByFour, + CommonAspectRatio::TallFourByFive, + CommonAspectRatio::TallNineBySixteen, + CommonAspectRatio::TallNineByTwentyOne, + CommonAspectRatio::Tall, + ]; + for ar in tall_ars { + let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1p5_image_request() }; + let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") }; + assert!(matches!(plan.image_size, Some(GS::Tall)), "expected Tall for {:?}", ar); + } + } + + // ── Quality ────────────────────────────────────────────────────────────── + + #[test] + fn quality_defaults_to_medium() { + let request = base_gpt_image_1p5_image_request(); + let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") }; + assert!(matches!(plan.quality, GptImage1p5MultiFunctionImageGenQuality::Medium)); + } + + // ── Num images ────────────────────────────────────────────────────────────── + + #[test] + fn num_images_zero_is_always_error() { + for strategy in [ + RequestMismatchMitigationStrategy::ErrorOut, + RequestMismatchMitigationStrategy::PayMoreUpgrade, + RequestMismatchMitigationStrategy::PayLessDowngrade, + ] { + let request = GenerateImageRequest { + image_batch_count: Some(0), + request_mismatch_mitigation_strategy: strategy, + ..base_gpt_image_1p5_image_request() + }; + let result = request.build(); + assert!( + matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))), + "expected UserRequestedZeroGenerations with {:?}", strategy, + ); + } + } + + #[test] + fn num_images_direct_mapping() { + let cases = [ + (1, GN::One), + (2, GN::Two), + (3, GN::Three), + (4, GN::Four), + ]; + for (count, expected) in cases { + let request = GenerateImageRequest { image_batch_count: Some(count), ..base_gpt_image_1p5_image_request() }; + let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") }; + assert!( + std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected), + "expected {:?} for count {}", expected, count, + ); + } + } + + #[test] + fn num_images_out_of_range_error_out() { + let request = GenerateImageRequest { + image_batch_count: Some(5), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + ..base_gpt_image_1p5_image_request() + }; + let result = request.build(); + assert!(matches!( + result, + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. })) + )); + } + + #[test] + fn num_images_out_of_range_clamps_to_four() { + for strategy in [ + RequestMismatchMitigationStrategy::PayMoreUpgrade, + RequestMismatchMitigationStrategy::PayLessDowngrade, + ] { + let request = GenerateImageRequest { + image_batch_count: Some(5), + request_mismatch_mitigation_strategy: strategy, + ..base_gpt_image_1p5_image_request() + }; + let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") }; + assert!( + matches!(plan.num_images, GN::Four), + "expected Four for count 5 with {:?}", strategy, + ); + } + } + + // ── Image inputs ───────────────────────────────────────────────────────── + + #[test] + fn url_image_inputs_returns_error() { + let urls = vec!["https://example.com/image.jpg".to_string()]; + let request = GenerateImageRequest { + image_inputs: Some(ImageListRef::Urls(&urls)), + ..base_gpt_image_1p5_image_request() + }; + let result = request.build(); + assert!(matches!( + result, + Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens)) + )); + } + + #[test] + fn no_image_inputs_is_text_to_image_mode() { + let request = GenerateImageRequest { image_inputs: None, ..base_gpt_image_1p5_image_request() }; + let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") }; + assert!(plan.image_inputs.is_none()); + } + + #[test] + fn media_token_image_inputs_is_edit_mode() { + let tokens = vec![]; + let request = GenerateImageRequest { + image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)), + ..base_gpt_image_1p5_image_request() + }; + let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") }; + assert!(plan.image_inputs.is_some()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana.rs new file mode 100644 index 0000000000..fbb79ec504 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana.rs @@ -0,0 +1,389 @@ +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::image_list_ref::ImageListRef; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::client_error::ClientError; +use crate::generate::generate_image::generate_image_request::GenerateImageRequest; +use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::{ + NanoBananaMultiFunctionImageGenAspectRatio, NanoBananaMultiFunctionImageGenNumImages, +}; +use tokens::tokens::media_files::MediaFileToken; + +#[derive(Debug, Clone)] +pub struct PlanArtcraftNanaBanana<'a> { + pub prompt: Option<&'a str>, + /// Input images for image editing. None means text-to-image mode. + pub image_inputs: Option<&'a Vec>, + pub aspect_ratio: Option, + pub num_images: NanoBananaMultiFunctionImageGenNumImages, + pub idempotency_token: String, +} + +pub fn plan_generate_image_artcraft_nano_banana<'a>( + request: &'a GenerateImageRequest<'a>, +) -> Result, ArtcraftRouterError> { + let strategy = request.request_mismatch_mitigation_strategy; + + let is_edit_mode = request.image_inputs.is_some(); + let image_inputs = resolve_image_list_ref(request.image_inputs)?; + let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?; + let num_images = plan_num_images(request.image_batch_count, strategy)?; + + Ok(PlanArtcraftNanaBanana { + prompt: request.prompt, + image_inputs, + aspect_ratio, + num_images, + idempotency_token: request.get_or_generate_idempotency_token(), + }) +} + +fn resolve_image_list_ref<'a>( + image_list_ref: Option>, +) -> Result>, ArtcraftRouterError> { + match image_list_ref { + None => Ok(None), + Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)), + Some(ImageListRef::Urls(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens)) + } + } +} + +// Nano Banana supported aspect ratios: +// Text-to-image: OneByOne, FiveByFour, FourByThree, ThreeByTwo, SixteenByNine, TwentyOneByNine, +// FourByFive, ThreeByFour, TwoByThree, NineBySixteen +// Image editing: Auto (uses source image AR), plus all text-to-image ratios +fn plan_aspect_ratio( + aspect_ratio: Option, + is_edit_mode: bool, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use NanoBananaMultiFunctionImageGenAspectRatio as NbAr; + match aspect_ratio { + // No preference — let the model use its default + None => Ok(None), + + // Auto: only valid in edit mode (use source image dimensions) + Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => { + if is_edit_mode { + Ok(Some(NbAr::Auto)) + } else { + // Auto not valid for text-to-image; fall back to square default + Ok(Some(NbAr::OneByOne)) + } + } + + // Direct mappings + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(NbAr::OneByOne)), + Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(NbAr::FiveByFour)), + Some(CommonAspectRatio::WideFourByThree) => Ok(Some(NbAr::FourByThree)), + Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(NbAr::ThreeByTwo)), + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(NbAr::SixteenByNine)), + Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(NbAr::TwentyOneByNine)), + Some(CommonAspectRatio::TallFourByFive) => Ok(Some(NbAr::FourByFive)), + Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(NbAr::ThreeByFour)), + Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(NbAr::TwoByThree)), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(NbAr::NineBySixteen)), + + // Mismatch — TallNineByTwentyOne has no direct equivalent + Some(unsupported) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade + | RequestMismatchMitigationStrategy::PayLessDowngrade => { + Ok(Some(nearest_aspect_ratio(unsupported))) + } + }, + } +} + +/// Pick the nearest supported aspect ratio for unsupported inputs. +fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> NanoBananaMultiFunctionImageGenAspectRatio { + use NanoBananaMultiFunctionImageGenAspectRatio as NbAr; + match aspect_ratio { + // TallNineByTwentyOne (0.43) — nearest is NineBySixteen (0.5625) + CommonAspectRatio::TallNineByTwentyOne => NbAr::NineBySixteen, + _ => NbAr::OneByOne, + } +} + +// Nano Banana supports 1, 2, 3, and 4 images. + +fn plan_num_images( + image_batch_count: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + use NanoBananaMultiFunctionImageGenNumImages as NbN; + let count = image_batch_count.unwrap_or(1); + match count { + 0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)), + 1 => Ok(NbN::One), + 2 => Ok(NbN::Two), + 3 => Ok(NbN::Three), + 4 => Ok(NbN::Four), + _ => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "image_batch_count", + value: format!("{}", count), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(NbN::Four), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(NbN::Four), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::common_aspect_ratio::CommonAspectRatio; + use crate::api::image_list_ref::ImageListRef; + use crate::errors::artcraft_router_error::ArtcraftRouterError; + use crate::errors::client_error::ClientError; + use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan; + use crate::test_helpers::base_nano_banana_image_request; + use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::{ + NanoBananaMultiFunctionImageGenAspectRatio as NbAr, + NanoBananaMultiFunctionImageGenNumImages as NbN, + }; + + // ── Aspect ratio ──────────────────────────────────────────────────────────── + + #[test] + fn aspect_ratio_none_is_none() { + let request = GenerateImageRequest { aspect_ratio: None, ..base_nano_banana_image_request() }; + let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") }; + assert!(plan.aspect_ratio.is_none()); + } + + #[test] + fn aspect_ratio_direct_square() { + for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] { + let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_nano_banana_image_request() }; + let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") }; + assert!(matches!(plan.aspect_ratio, Some(NbAr::OneByOne)), "expected OneByOne for {:?}", ar); + } + } + + #[test] + fn aspect_ratio_direct_wide() { + let cases = [ + (CommonAspectRatio::WideFiveByFour, NbAr::FiveByFour), + (CommonAspectRatio::WideFourByThree, NbAr::FourByThree), + (CommonAspectRatio::WideThreeByTwo, NbAr::ThreeByTwo), + (CommonAspectRatio::WideSixteenByNine, NbAr::SixteenByNine), + (CommonAspectRatio::Wide, NbAr::SixteenByNine), + (CommonAspectRatio::WideTwentyOneByNine, NbAr::TwentyOneByNine), + ]; + for (common, expected) in cases { + let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_nano_banana_image_request() }; + let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") }; + assert!( + matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)), + "expected {:?} for {:?}", expected, common, + ); + } + } + + #[test] + fn aspect_ratio_direct_tall() { + let cases = [ + (CommonAspectRatio::TallFourByFive, NbAr::FourByFive), + (CommonAspectRatio::TallThreeByFour, NbAr::ThreeByFour), + (CommonAspectRatio::TallTwoByThree, NbAr::TwoByThree), + (CommonAspectRatio::TallNineBySixteen, NbAr::NineBySixteen), + (CommonAspectRatio::Tall, NbAr::NineBySixteen), + ]; + for (common, expected) in cases { + let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_nano_banana_image_request() }; + let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") }; + assert!( + matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)), + "expected {:?} for {:?}", expected, common, + ); + } + } + + #[test] + fn aspect_ratio_auto_in_edit_mode_yields_auto() { + for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] { + let tokens = vec![]; + let request = GenerateImageRequest { + aspect_ratio: Some(auto_ar), + image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)), + ..base_nano_banana_image_request() + }; + let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") }; + assert!( + matches!(plan.aspect_ratio, Some(NbAr::Auto)), + "expected Auto in edit mode for {:?}", auto_ar, + ); + } + } + + #[test] + fn aspect_ratio_auto_in_text_to_image_mode_falls_back_to_square() { + for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] { + let request = GenerateImageRequest { + aspect_ratio: Some(auto_ar), + image_inputs: None, + ..base_nano_banana_image_request() + }; + let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") }; + assert!( + matches!(plan.aspect_ratio, Some(NbAr::OneByOne)), + "expected OneByOne fallback in text-to-image for {:?}", auto_ar, + ); + } + } + + #[test] + fn aspect_ratio_unsupported_error_out() { + // TallNineByTwentyOne has no direct equivalent + let request = GenerateImageRequest { + aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + ..base_nano_banana_image_request() + }; + let result = request.build(); + assert!(matches!( + result, + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. })) + )); + } + + #[test] + fn aspect_ratio_unsupported_nearest_match() { + // TallNineByTwentyOne (0.43) is nearest to NineBySixteen (0.5625) + for strategy in [ + RequestMismatchMitigationStrategy::PayMoreUpgrade, + RequestMismatchMitigationStrategy::PayLessDowngrade, + ] { + let request = GenerateImageRequest { + aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne), + request_mismatch_mitigation_strategy: strategy, + ..base_nano_banana_image_request() + }; + let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") }; + assert!( + matches!(plan.aspect_ratio, Some(NbAr::NineBySixteen)), + "expected NineBySixteen nearest match with {:?}", strategy, + ); + } + } + + // ── Num images ────────────────────────────────────────────────────────────── + + #[test] + fn num_images_zero_is_always_error() { + for strategy in [ + RequestMismatchMitigationStrategy::ErrorOut, + RequestMismatchMitigationStrategy::PayMoreUpgrade, + RequestMismatchMitigationStrategy::PayLessDowngrade, + ] { + let request = GenerateImageRequest { + image_batch_count: Some(0), + request_mismatch_mitigation_strategy: strategy, + ..base_nano_banana_image_request() + }; + let result = request.build(); + assert!( + matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))), + "expected UserRequestedZeroGenerations with {:?}", strategy, + ); + } + } + + #[test] + fn num_images_direct_mapping() { + let cases = [ + (1, NbN::One), + (2, NbN::Two), + (3, NbN::Three), + (4, NbN::Four), + ]; + for (count, expected) in cases { + let request = GenerateImageRequest { image_batch_count: Some(count), ..base_nano_banana_image_request() }; + let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") }; + assert!( + std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected), + "expected {:?} for count {}", expected, count, + ); + } + } + + #[test] + fn num_images_out_of_range_error_out() { + let request = GenerateImageRequest { + image_batch_count: Some(5), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + ..base_nano_banana_image_request() + }; + let result = request.build(); + assert!(matches!( + result, + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. })) + )); + } + + #[test] + fn num_images_out_of_range_clamps_to_four() { + for strategy in [ + RequestMismatchMitigationStrategy::PayMoreUpgrade, + RequestMismatchMitigationStrategy::PayLessDowngrade, + ] { + let request = GenerateImageRequest { + image_batch_count: Some(5), + request_mismatch_mitigation_strategy: strategy, + ..base_nano_banana_image_request() + }; + let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") }; + assert!( + matches!(plan.num_images, NbN::Four), + "expected Four for count 5 with {:?}", strategy, + ); + } + } + + // ── Image inputs ───────────────────────────────────────────────────────── + + #[test] + fn url_image_inputs_returns_error() { + let urls = vec!["https://example.com/image.jpg".to_string()]; + let request = GenerateImageRequest { + image_inputs: Some(ImageListRef::Urls(&urls)), + ..base_nano_banana_image_request() + }; + let result = request.build(); + assert!(matches!( + result, + Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens)) + )); + } + + #[test] + fn no_image_inputs_is_text_to_image_mode() { + let request = GenerateImageRequest { image_inputs: None, ..base_nano_banana_image_request() }; + let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") }; + assert!(plan.image_inputs.is_none()); + } + + #[test] + fn media_token_image_inputs_is_edit_mode() { + let tokens = vec![]; + let request = GenerateImageRequest { + image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)), + ..base_nano_banana_image_request() + }; + let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") }; + assert!(plan.image_inputs.is_some()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs b/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs index 806788f562..9a5b2c1972 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs @@ -85,6 +85,34 @@ pub fn base_seedream_4p5_image_request() -> GenerateImageRequest<'static> { } } +pub fn base_nano_banana_image_request() -> GenerateImageRequest<'static> { + GenerateImageRequest { + model: CommonImageModel::NanaBanana, + provider: Provider::Artcraft, + prompt: Some("a cat in space"), + image_inputs: None, + resolution: None, + aspect_ratio: None, + image_batch_count: None, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + idempotency_token: None, + } +} + +pub fn base_gpt_image_1p5_image_request() -> GenerateImageRequest<'static> { + GenerateImageRequest { + model: CommonImageModel::GptImage1p5, + provider: Provider::Artcraft, + prompt: Some("a cat in space"), + image_inputs: None, + resolution: None, + aspect_ratio: None, + image_batch_count: None, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + idempotency_token: None, + } +} + pub fn base_video_request() -> GenerateVideoRequest<'static> { GenerateVideoRequest { model: CommonVideoModel::Seedance2p0, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_artcraft_gpt_image_1p5_edit.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_artcraft_gpt_image_1p5_edit.rs deleted file mode 100644 index 7c2cdd8087..0000000000 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_artcraft_gpt_image_1p5_edit.rs +++ /dev/null @@ -1,150 +0,0 @@ -use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_gpt_image_1::aspect_ratio_to_artcraft_gpt_image_1; -use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_gpt_image_1p5::aspect_ratio_to_artcraft_gpt_image_1p5; -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::{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}; -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::multi_function::gpt_image_1p5_multi_function_image_gen::{GptImage1p5MultiFunctionImageGenNumImages, GptImage1p5MultiFunctionImageGenQuality, GptImage1p5MultiFunctionImageGenRequest, GptImage1p5MultiFunctionImageGenSize}; -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::gpt_image_1_edit_image::gpt_image_1_edit_image; -use artcraft_client::endpoints::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen_image::gpt_image_1p5_multi_function_image_gen; -use tauri::AppHandle; - -const MAX_IMAGES: usize = 4; - -pub async fn handle_artcraft_gpt_image_1p5_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 gpt-image-1.5 (edit) ..."); - - let uuid_idempotency_token = generate_random_uuid(); - - let image_quality = request.image_quality - .map(|quality| match quality { - EditImageQuality::Auto => GptImage1p5MultiFunctionImageGenQuality::High, - EditImageQuality::High => GptImage1p5MultiFunctionImageGenQuality::High, - EditImageQuality::Medium => GptImage1p5MultiFunctionImageGenQuality::Medium, - EditImageQuality::Low => GptImage1p5MultiFunctionImageGenQuality::Low, - }); - - let image_size = get_aspect_ratio(request); - - let num_images = match request.image_count { - None => None, - Some(1) => Some(GptImage1p5MultiFunctionImageGenNumImages::One), - Some(2) => Some(GptImage1p5MultiFunctionImageGenNumImages::Two), - Some(3) => Some(GptImage1p5MultiFunctionImageGenNumImages::Three), - Some(4) => Some(GptImage1p5MultiFunctionImageGenNumImages::Four), - Some(other) => { - return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages { - min: 1, - max: 4, - requested: other, - })); - }, - }; - - 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 = GptImage1p5MultiFunctionImageGenRequest { - uuid_idempotency_token, - prompt: Some(request.prompt.clone()), - image_media_tokens: Some(media_tokens), - quality: image_quality, - image_size, - num_images, - // Only for inpainting - mask_image_token: None, - // Not provided - input_fidelity: None, - output_format: None, - background: None, - }; - - let result = gpt_image_1p5_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 gpt-image-1.5. Job token: {}", - enqueued.inference_job_token); - enqueued.inference_job_token - } - Err(err) => { - error!("Failed to use Artcraft gpt-image-1.5: {:?}", err); - return Err(GenerateError::from(err)); - } - }; - - Ok(TaskEnqueueSuccess { - provider: GenerationProvider::Artcraft, - model: Some(GenerationModel::GptImage1p5), - 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_gpt_image_1p5(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(GptImage1p5MultiFunctionImageGenSize::Square), - EditImageSize::Square => Some(GptImage1p5MultiFunctionImageGenSize::Square), - EditImageSize::Wide => Some(GptImage1p5MultiFunctionImageGenSize::Wide), - EditImageSize::Tall => Some(GptImage1p5MultiFunctionImageGenSize::Tall), - } - } - - None -} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_artcraft_nano_banana_edit.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_artcraft_nano_banana_edit.rs deleted file mode 100644 index cda171ea70..0000000000 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/artcraft/handle_artcraft_nano_banana_edit.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_nano_banana::aspect_ratio_to_artcraft_nano_banana; -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::{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}; -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_multi_function_image_gen::{NanoBananaMultiFunctionImageGenAspectRatio, NanoBananaMultiFunctionImageGenNumImages, NanoBananaMultiFunctionImageGenRequest}; -use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{NanoBananaProMultiFunctionImageGenAspectRatio, 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_multi_function_image_gen_image::nano_banana_multi_function_image_gen; -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_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 aspect_ratio = get_aspect_ratio(request); - - let num_images = match request.image_count { - None => None, - Some(1) => Some(NanoBananaMultiFunctionImageGenNumImages::One), - Some(2) => Some(NanoBananaMultiFunctionImageGenNumImages::Two), - Some(3) => Some(NanoBananaMultiFunctionImageGenNumImages::Three), - Some(4) => Some(NanoBananaMultiFunctionImageGenNumImages::Four), - Some(other) => { - return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages { - min: 1, - max: 4, - requested: other, - })); - }, - }; - - 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 = NanoBananaMultiFunctionImageGenRequest { - uuid_idempotency_token, - prompt: Some(request.prompt.clone()), - image_media_tokens: Some(media_tokens), - num_images, - aspect_ratio, - }; - - let result = nano_banana_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::Gemini25Flash), - 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(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(NanoBananaMultiFunctionImageGenAspectRatio::Auto), - EditImageSize::Square => Some(NanoBananaMultiFunctionImageGenAspectRatio::OneByOne), - EditImageSize::Wide => Some(NanoBananaMultiFunctionImageGenAspectRatio::SixteenByNine), - EditImageSize::Tall => Some(NanoBananaMultiFunctionImageGenAspectRatio::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 f9e599d192..dbbb34958f 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 @@ -1,8 +1,6 @@ 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_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_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::enqueue::task_enqueue_success::TaskEnqueueSuccess; @@ -25,11 +23,11 @@ pub async fn handle_image_edit_artcraft( match model { ImageEditModel::FluxProKontextMax => handle_artcraft_flux_kontext_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await, ImageEditModel::Gemini25Flash | ImageEditModel::NanoBanana => { - handle_artcraft_nano_banana_edit( request, app, app_data_root, app_env_configs, storyteller_creds_manager).await + handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBanana, GenerationModel::NanoBanana).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::GptImage1p5 => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::GptImage1p5, GenerationModel::GptImage1p5).await, ImageEditModel::Seedream4 => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream4, GenerationModel::Seedream4).await, ImageEditModel::Seedream4p5 => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream4p5, GenerationModel::Seedream4p5).await, } 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 7a9b34e1ee..49b2aaf980 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,7 +1,5 @@ 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; pub (super) mod handle_image_edit_artcraft; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_artcraft_gpt_image_1p5_text_to_image.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_artcraft_gpt_image_1p5_text_to_image.rs deleted file mode 100644 index 6997d8b3d7..0000000000 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_artcraft_gpt_image_1p5_text_to_image.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_gpt_image_1p5::aspect_ratio_to_artcraft_gpt_image_1p5; -use crate::core::commands::enqueue::generate_error::GenerateError; -use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, 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 artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{GptImage1p5MultiFunctionImageGenNumImages, GptImage1p5MultiFunctionImageGenQuality, GptImage1p5MultiFunctionImageGenRequest, GptImage1p5MultiFunctionImageGenSize}; -use enums::common::generation_provider::GenerationProvider; -use enums::tauri::tasks::task_type::TaskType; -use idempotency::uuid::generate_random_uuid; -use log::{error, info}; -use std::time::Duration; -use artcraft_client::endpoints::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen_image::gpt_image_1p5_multi_function_image_gen; -use tauri::AppHandle; - -pub async fn handle_artcraft_gpt_image_1p5_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::needs_storyteller_credentials()); - }, - }; - - info!("Calling Artcraft gpt-image-1.5 ..."); - - let uuid_idempotency_token = generate_random_uuid(); - - let aspect_ratio = get_aspect_ratio(request); - - let num_images = match request.number_images { - None => None, - Some(1) => Some(GptImage1p5MultiFunctionImageGenNumImages::One), - Some(2) => Some(GptImage1p5MultiFunctionImageGenNumImages::Two), - Some(3) => Some(GptImage1p5MultiFunctionImageGenNumImages::Three), - Some(4) => Some(GptImage1p5MultiFunctionImageGenNumImages::Four), - // TODO: Error - //Some(other) => { - // return Err(InternalImageError::InvalidNumberOfRequestedImages { - // min: 1, - // max: 4, - // requested: other, - // }); - //}, - _ => Some(GptImage1p5MultiFunctionImageGenNumImages::One), // Default to one image if invalid number - }; - - let request = GptImage1p5MultiFunctionImageGenRequest { - uuid_idempotency_token, - prompt: request.prompt.clone(), - image_size: aspect_ratio, - num_images, - image_media_tokens: request.image_media_tokens.clone(), - mask_image_token: None, - input_fidelity: None, - // Not provided - output_format: None, - background: None, - quality: None, - }; - - let result = gpt_image_1p5_multi_function_image_gen( - &app_env_configs.storyteller_host, - Some(&creds), - request, - ).await; - - let job_id = match result { - Ok(enqueued) => { - info!("Successfully enqueued Artcraft gpt-image-1.5. Job token: {}", - enqueued.inference_job_token); - enqueued.inference_job_token - } - Err(err) => { - error!("Failed to use Artcraft gpt-image-1.5: {:?}", err); - return Err(GenerateError::from(err)); - } - }; - - Ok(TaskEnqueueSuccess { - provider: GenerationProvider::Artcraft, - model: Some(GenerationModel::GptImage1p5), - 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_gpt_image_1p5(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(GptImage1p5MultiFunctionImageGenSize::Square), - TextToImageSize::Square => Some(GptImage1p5MultiFunctionImageGenSize::Square), - TextToImageSize::Wide => Some(GptImage1p5MultiFunctionImageGenSize::Wide), - TextToImageSize::Tall => Some(GptImage1p5MultiFunctionImageGenSize::Tall), - } - } - - None -} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_artcraft_nano_banana_text_to_image.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_artcraft_nano_banana_text_to_image.rs deleted file mode 100644 index 298dd13178..0000000000 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/artcraft/handle_artcraft_nano_banana_text_to_image.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_nano_banana::aspect_ratio_to_artcraft_nano_banana; -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::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 artcraft_api_defs::generate::image::edit::gemini_25_flash_edit_image::{Gemini25FlashEditImageNumImages, Gemini25FlashEditImageRequest}; -use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::{NanoBananaMultiFunctionImageGenAspectRatio, NanoBananaMultiFunctionImageGenNumImages, NanoBananaMultiFunctionImageGenRequest}; -use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::NanoBananaProMultiFunctionImageGenAspectRatio; -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 std::time::Duration; -use artcraft_client::endpoints::generate::image::multi_function::nano_banana_multi_function_image_gen_image::nano_banana_multi_function_image_gen; -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_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..."); - - let uuid_idempotency_token = generate_random_uuid(); - - let num_images = match request.number_images { - None => None, - Some(1) => Some(NanoBananaMultiFunctionImageGenNumImages::One), - Some(2) => Some(NanoBananaMultiFunctionImageGenNumImages::Two), - Some(3) => Some(NanoBananaMultiFunctionImageGenNumImages::Three), - Some(4) => Some(NanoBananaMultiFunctionImageGenNumImages::Four), - _ => Some(NanoBananaMultiFunctionImageGenNumImages::One), // Default to one image if invalid number - }; - - let aspect_ratio = get_aspect_ratio(request); - - let request = NanoBananaMultiFunctionImageGenRequest { - uuid_idempotency_token, - prompt: request.prompt.clone(), - image_media_tokens: request.image_media_tokens.clone(), - num_images, - aspect_ratio, - }; - - let result = nano_banana_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::NanoBanana), - 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(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(NanoBananaMultiFunctionImageGenAspectRatio::Auto), - TextToImageSize::Square => Some(NanoBananaMultiFunctionImageGenAspectRatio::OneByOne), - TextToImageSize::Wide => Some(NanoBananaMultiFunctionImageGenAspectRatio::SixteenByNine), - TextToImageSize::Tall => Some(NanoBananaMultiFunctionImageGenAspectRatio::NineBySixteen), - } - } - - 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 086876795c..f593d89739 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 @@ -5,9 +5,7 @@ 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_text_to_image::handle_artcraft_flux_pro_1p1_text_to_image; 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_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::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; @@ -42,9 +40,9 @@ pub async fn handle_text_to_image_artcraft( TextToImageModel::FluxPro11 =>handle_artcraft_flux_pro_1p1_text_to_image(request, app_env_configs, storyteller_creds_manager).await, TextToImageModel::FluxPro11Ultra => handle_artcraft_flux_pro_1p1_ultra_text_to_image(request, app_env_configs, storyteller_creds_manager).await, TextToImageModel::GptImage1 => handle_artcraft_gpt_image_1_text_to_image(request, app_env_configs, storyteller_creds_manager).await, - TextToImageModel::GptImage1p5 => handle_artcraft_gpt_image_1p5_text_to_image(request, app_env_configs, storyteller_creds_manager).await, + TextToImageModel::GptImage1p5 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::GptImage1p5, GenerationModel::GptImage1p5).await, TextToImageModel::Gemini25Flash | TextToImageModel::NanoBanana => { - handle_artcraft_nano_banana_text_to_image(request, app_env_configs, storyteller_creds_manager).await + handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBanana, GenerationModel::NanoBanana).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_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream4, GenerationModel::Seedream4).await, 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 9c5ec3c4d3..2671f4be65 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,10 +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_text_to_image; -mod handle_artcraft_flux_pro_1p1_ultra_text_to_image; mod handle_artcraft_flux_1_dev_text_to_image; +mod handle_artcraft_flux_1_schnell_text_to_image; mod handle_artcraft_flux_pro_1p1_text_to_image; -mod handle_artcraft_flux_1_schnell_text_to_image; \ No newline at end of file +mod handle_artcraft_flux_pro_1p1_ultra_text_to_image; +mod handle_artcraft_gpt_image_1_text_to_image; \ No newline at end of file