From 520117d5d37e355eadbafa9929aab5ee57e0aeb6 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 4 Mar 2026 09:19:34 -0500 Subject: [PATCH] Add new models: Seedream 5 Lite, Nano Banana 2 (#1088) --- ...eedream_5_lite_multi_function_image_gen.rs | 68 ++++ .../src/generate/image/multi_function/mod.rs | 2 + .../nano_banana_2_multi_function_image_gen.rs | 74 ++++ ...m_5_lite_multi_function_image_gen_image.rs | 18 + .../generate/image/multi_function/mod.rs | 2 + ...banana_2_multi_function_image_gen_image.rs | 18 + .../src/api/common_image_model.rs | 4 + ...imate_image_cost_artcraft_nano_banana_2.rs | 79 +++++ ...ate_image_cost_artcraft_seedream_5_lite.rs | 66 ++++ .../generate_image/cost/artcraft/mod.rs | 2 + .../generate_image_artcraft_nano_banana_2.rs | 67 ++++ ...generate_image_artcraft_seedream_5_lite.rs | 65 ++++ .../generate_image/execute/artcraft/mod.rs | 2 + .../generate_image/generate_image_request.rs | 12 +- .../generate_image/image_generation_plan.rs | 22 ++ .../generate_image/plan/artcraft/mod.rs | 2 + ...n_generate_image_artcraft_nano_banana_2.rs | 289 ++++++++++++++++ ...generate_image_artcraft_seedream_5_lite.rs | 211 ++++++++++++ .../artcraft_router/src/test_helpers.rs | 30 ++ .../image/http_nano_banana_2_edit_image.rs | 79 +++++ .../image/http_nano_banana_2_text_to_image.rs | 77 +++++ .../http/image/http_seedream_5_edit_image.rs | 49 +++ .../image/http_seedream_5_text_to_image.rs | 46 +++ .../fal_client/src/requests/http/image/mod.rs | 4 + .../fal_client/src/requests/http/mod.rs | 1 + .../fal_client/src/requests/mod.rs | 5 +- ...nce_seedream_v5_lite_edit_image_webhook.rs | 158 +++++++++ ...nqueue_nano_banana_2_edit_image_webhook.rs | 181 ++++++++++ .../src/requests/webhook/image/edit/mod.rs | 2 + ..._seedream_v5_lite_text_to_image_webhook.rs | 151 ++++++++ ...eue_nano_banana_2_text_to_image_webhook.rs | 173 ++++++++++ .../src/requests/webhook/image/text/mod.rs | 2 + .../artcraft/handle_image_edit_artcraft.rs | 2 + .../image_edit/enqueue_edit_image_command.rs | 16 +- .../enqueue/image_edit/image_edit_models.rs | 2 + .../artcraft/handle_text_to_image_artcraft.rs | 2 + .../enqueue_text_to_image_command.rs | 4 + .../text_to_image/text_to_image_models.rs | 2 + .../core/events/generation_events/common.rs | 4 + .../common/generation/common_image_model.rs | 4 + .../public/enums/src/common/model_type.rs | 20 +- .../image/estimate_image_cost_handler.rs | 9 +- ...5_lite_multi_function_image_gen_handler.rs | 301 ++++++++++++++++ .../generate/image/multi_function/mod.rs | 2 + ...nana_2_multi_function_image_gen_handler.rs | 324 ++++++++++++++++++ .../application_routes/generate_routes.rs | 10 + 46 files changed, 2651 insertions(+), 12 deletions(-) create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/generate/image/multi_function/bytedance_seedream_5_lite_multi_function_image_gen.rs create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/generate/image/multi_function/nano_banana_2_multi_function_image_gen.rs create mode 100644 crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/image/multi_function/bytedance_seedream_5_lite_multi_function_image_gen_image.rs create mode 100644 crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/image/multi_function/nano_banana_2_multi_function_image_gen_image.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana_2.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_5_lite.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana_2.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_5_lite.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana_2.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_5_lite.rs create mode 100644 crates/api_clients/fal_client/src/requests/http/image/http_nano_banana_2_edit_image.rs create mode 100644 crates/api_clients/fal_client/src/requests/http/image/http_nano_banana_2_text_to_image.rs create mode 100644 crates/api_clients/fal_client/src/requests/http/image/http_seedream_5_edit_image.rs create mode 100644 crates/api_clients/fal_client/src/requests/http/image/http_seedream_5_text_to_image.rs create mode 100644 crates/api_clients/fal_client/src/requests/http/image/mod.rs create mode 100644 crates/api_clients/fal_client/src/requests/http/mod.rs create mode 100644 crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_bytedance_seedream_v5_lite_edit_image_webhook.rs create mode 100644 crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_nano_banana_2_edit_image_webhook.rs create mode 100644 crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_bytedance_seedream_v5_lite_text_to_image_webhook.rs create mode 100644 crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_nano_banana_2_text_to_image_webhook.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/multi_function/bytedance_seedream_5_lite_multi_function_image_gen_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/multi_function/nano_banana_2_multi_function_image_gen_handler.rs diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/image/multi_function/bytedance_seedream_5_lite_multi_function_image_gen.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/image/multi_function/bytedance_seedream_5_lite_multi_function_image_gen.rs new file mode 100644 index 000000000..3d2198941 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/image/multi_function/bytedance_seedream_5_lite_multi_function_image_gen.rs @@ -0,0 +1,68 @@ +use serde_derive::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +pub const BYTEDANCE_SEEDREAM_5_LITE_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/bytedance_seedream_5_lite"; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct BytedanceSeedream5LiteMultiFunctionImageGenRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Text prompt to generate the image from. + pub prompt: Option, + + /// Image media tokens to include in the editing context. + /// If present, we're doing image editing (image-to-image / image-editing) + /// If absent, we're doing image generation (text-to-image) + pub image_media_tokens: Option>, + + /// Number of images to generate. Default is one. + pub num_images: Option, + + pub max_images: Option, + + pub image_size: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum BytedanceSeedream5LiteMultiFunctionImageGenNumImages { + One, // Default + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum BytedanceSeedream5LiteMultiFunctionImageGenMaxImages { + One, // Default + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum BytedanceSeedream5LiteMultiFunctionImageGenImageSize { + // Square + Square, + SquareHd, + // Tall + PortraitFourThree, + PortraitSixteenNine, + // Wide + LandscapeFourThree, + LandscapeSixteenNine, + // Auto (NB: 5 Lite max is auto_3K, not auto_4K like V4.5) + Auto2k, + Auto3k, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct BytedanceSeedream5LiteMultiFunctionImageGenResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/image/multi_function/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/image/multi_function/mod.rs index 43e7b65fb..9df835186 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/image/multi_function/mod.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/image/multi_function/mod.rs @@ -1,5 +1,7 @@ pub mod bytedance_seedream_v4_multi_function_image_gen; pub mod bytedance_seedream_v4p5_multi_function_image_gen; +pub mod bytedance_seedream_5_lite_multi_function_image_gen; pub mod gpt_image_1p5_multi_function_image_gen; +pub mod nano_banana_2_multi_function_image_gen; pub mod nano_banana_multi_function_image_gen; pub mod nano_banana_pro_multi_function_image_gen; \ No newline at end of file diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/image/multi_function/nano_banana_2_multi_function_image_gen.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/image/multi_function/nano_banana_2_multi_function_image_gen.rs new file mode 100644 index 000000000..4e22dd4ae --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/image/multi_function/nano_banana_2_multi_function_image_gen.rs @@ -0,0 +1,74 @@ +use serde_derive::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +pub const NANO_BANANA_2_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/nano_banana_2"; + +#[derive(Serialize, Deserialize, ToSchema, Debug)] +pub struct NanaBanana2MultiFunctionImageGenRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Text prompt to generate the image from. + pub prompt: Option, + + /// Image media tokens to include in the editing context. + /// If present, we're doing image editing (image-to-image / image-editing) + /// If absent, we're doing image generation (text-to-image) + pub image_media_tokens: Option>, + + /// Number of images to generate. Default is one. + pub num_images: Option, + + /// Resolution of the image to generate. Default is OneK (1K). + pub resolution: Option, + + /// Aspect ratio of the images to generate. Default is "1:1" + pub aspect_ratio: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum NanaBanana2MultiFunctionImageGenNumImages { + One, // Default + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum NanaBanana2MultiFunctionImageGenImageResolution { + HalfK, + OneK, + TwoK, + FourK, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum NanaBanana2MultiFunctionImageGenAspectRatio { + // Auto (only for image editing) + Auto, // Default for image editing. + // Square + OneByOne, // Default for text-to-image + // Wide + FiveByFour, + FourByThree, + ThreeByTwo, + SixteenByNine, + TwentyOneByNine, + // Tall + FourByFive, + ThreeByFour, + TwoByThree, + NineBySixteen, +} + + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct NanaBanana2MultiFunctionImageGenResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/image/multi_function/bytedance_seedream_5_lite_multi_function_image_gen_image.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/image/multi_function/bytedance_seedream_5_lite_multi_function_image_gen_image.rs new file mode 100644 index 000000000..52bb84bef --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/image/multi_function/bytedance_seedream_5_lite_multi_function_image_gen_image.rs @@ -0,0 +1,18 @@ +use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; +use crate::error::storyteller_error::StorytellerError; +use crate::utils::api_host::ApiHost; +use crate::utils::basic_json_post_request::basic_json_post_request; +use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::{BytedanceSeedream5LiteMultiFunctionImageGenRequest, BytedanceSeedream5LiteMultiFunctionImageGenResponse, BYTEDANCE_SEEDREAM_5_LITE_MULTI_FUNCTION_IMAGE_GEN_PATH}; + +pub async fn bytedance_seedream_5_lite_multi_function_image_gen( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + request: BytedanceSeedream5LiteMultiFunctionImageGenRequest, +) -> Result { + Ok(basic_json_post_request( + api_host, + BYTEDANCE_SEEDREAM_5_LITE_MULTI_FUNCTION_IMAGE_GEN_PATH, + maybe_creds, + request, + ).await?) +} diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/image/multi_function/mod.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/image/multi_function/mod.rs index 190a87b48..cc3e6edc8 100644 --- a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/image/multi_function/mod.rs +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/image/multi_function/mod.rs @@ -1,5 +1,7 @@ pub mod bytedance_seedream_v4_multi_function_image_gen_image; pub mod bytedance_seedream_v4p5_multi_function_image_gen_image; +pub mod bytedance_seedream_5_lite_multi_function_image_gen_image; pub mod gpt_image_1p5_multi_function_image_gen_image; +pub mod nano_banana_2_multi_function_image_gen_image; pub mod nano_banana_multi_function_image_gen_image; pub mod nano_banana_pro_multi_function_image_gen_image; diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/image/multi_function/nano_banana_2_multi_function_image_gen_image.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/image/multi_function/nano_banana_2_multi_function_image_gen_image.rs new file mode 100644 index 000000000..2439adee1 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/image/multi_function/nano_banana_2_multi_function_image_gen_image.rs @@ -0,0 +1,18 @@ +use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; +use crate::error::storyteller_error::StorytellerError; +use crate::utils::api_host::ApiHost; +use crate::utils::basic_json_post_request::basic_json_post_request; +use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::{NanaBanana2MultiFunctionImageGenRequest, NanaBanana2MultiFunctionImageGenResponse, NANO_BANANA_2_MULTI_FUNCTION_IMAGE_GEN_PATH}; + +pub async fn nano_banana_2_multi_function_image_gen( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + request: NanaBanana2MultiFunctionImageGenRequest, +) -> Result { + Ok(basic_json_post_request( + api_host, + NANO_BANANA_2_MULTI_FUNCTION_IMAGE_GEN_PATH, + maybe_creds, + request, + ).await?) +} 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 9a8bbfeab..ca4977099 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 @@ -17,10 +17,14 @@ pub enum CommonImageModel { GptImage1p5, #[serde(rename = "nano_banana")] NanaBanana, + #[serde(rename = "nano_banana_2")] + NanaBanana2, #[serde(rename = "nano_banana_pro")] NanaBananaPro, #[serde(rename = "seedream_4")] Seedream4, #[serde(rename = "seedream_4p5")] Seedream4p5, + #[serde(rename = "seedream_5_lite")] + Seedream5Lite, } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana_2.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana_2.rs new file mode 100644 index 000000000..d9ce31944 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana_2.rs @@ -0,0 +1,79 @@ +use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::{ + NanaBanana2MultiFunctionImageGenImageResolution, NanaBanana2MultiFunctionImageGenNumImages, +}; + +use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate; +use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::PlanArtcraftNanaBanana2; + +pub(crate) fn estimate_image_cost_artcraft_nano_banana_2( + plan: &PlanArtcraftNanaBanana2<'_>, +) -> ImageGenerationCostEstimate { + // Pricing: $0.08/image at 0.5K, $0.15/image at 1K or 2K, $0.30/image at 4K. + // (Stored as USD cents.) + let cost_per_image: u64 = match plan.resolution { + Some(NanaBanana2MultiFunctionImageGenImageResolution::HalfK) => 8, + Some(NanaBanana2MultiFunctionImageGenImageResolution::FourK) => 30, + _ => 15, // OneK, TwoK, or None (default 1K) + }; + + let num_images: u64 = match plan.num_images { + NanaBanana2MultiFunctionImageGenNumImages::One => 1, + NanaBanana2MultiFunctionImageGenNumImages::Two => 2, + NanaBanana2MultiFunctionImageGenNumImages::Three => 3, + NanaBanana2MultiFunctionImageGenNumImages::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::common_resolution::CommonResolution; + 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(resolution: Option, image_batch_count: u16) -> u64 { + let request = GenerateImageRequest { + model: CommonImageModel::NanaBanana2, + provider: Provider::Artcraft, + prompt: None, + image_inputs: None, + resolution, + aspect_ratio: None, + image_batch_count: Some(image_batch_count), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + generation_mode_mismatch_strategy: None, + 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() { + // 1K/2K: $0.15/image = 15 cents + assert_eq!(estimate_usd_cents(None, 1), 15); + assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 1), 15); + assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 1), 15); + assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 2), 30); + assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 4), 60); + + // 4K: $0.30/image = 30 cents + assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 1), 30); + assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 2), 60); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_5_lite.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_5_lite.rs new file mode 100644 index 000000000..a746fa320 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_5_lite.rs @@ -0,0 +1,66 @@ +use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::BytedanceSeedream5LiteMultiFunctionImageGenNumImages; + +use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate; +use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite; + +pub(crate) fn estimate_image_cost_artcraft_seedream_5_lite( + plan: &PlanArtcraftSeedream5Lite<'_>, +) -> 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 { + BytedanceSeedream5LiteMultiFunctionImageGenNumImages::One => 1, + BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Two => 2, + BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Three => 3, + BytedanceSeedream5LiteMultiFunctionImageGenNumImages::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::Seedream5Lite, + 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, + generation_mode_mismatch_strategy: None, + 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 c99417603..6edfe32f8 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 @@ -4,6 +4,8 @@ pub mod estimate_image_cost_artcraft_flux_pro_1p1; pub mod estimate_image_cost_artcraft_flux_pro_1p1_ultra; 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_2; 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; +pub mod estimate_image_cost_artcraft_seedream_5_lite; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana_2.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana_2.rs new file mode 100644 index 000000000..fd89850be --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana_2.rs @@ -0,0 +1,67 @@ +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_2::PlanArtcraftNanaBanana2; +use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::NanaBanana2MultiFunctionImageGenRequest; +use artcraft_client::endpoints::generate::image::multi_function::nano_banana_2_multi_function_image_gen_image::nano_banana_2_multi_function_image_gen; + +pub async fn execute_artcraft_nano_banana_2( + plan: &PlanArtcraftNanaBanana2<'_>, + artcraft_client: &RouterArtcraftClient, +) -> Result { + let request = NanaBanana2MultiFunctionImageGenRequest { + 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), + resolution: plan.resolution, + aspect_ratio: plan.aspect_ratio, + }; + + let response = nano_banana_2_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::common_resolution::CommonResolution; + 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_2_image_request, get_artcraft_client}; + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn test_text_to_image_nano_banana_2() { + let client = get_artcraft_client(); + let request = GenerateImageRequest { + aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + resolution: Some(CommonResolution::OneK), + image_batch_count: Some(1), + prompt: Some("a cat walking through a cyberpunk city at night"), + ..base_nano_banana_2_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. + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_5_lite.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_5_lite.rs new file mode 100644 index 000000000..4d2384e73 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_5_lite.rs @@ -0,0 +1,65 @@ +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_seedream_5_lite::PlanArtcraftSeedream5Lite; +use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::BytedanceSeedream5LiteMultiFunctionImageGenRequest; +use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen_image::bytedance_seedream_5_lite_multi_function_image_gen; + +pub async fn execute_artcraft_seedream_5_lite( + plan: &PlanArtcraftSeedream5Lite<'_>, + artcraft_client: &RouterArtcraftClient, +) -> Result { + let request = BytedanceSeedream5LiteMultiFunctionImageGenRequest { + 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), + image_size: plan.image_size, + max_images: None, + }; + + let response = bytedance_seedream_5_lite_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::generate::generate_image::generate_image_request::GenerateImageRequest; + use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan; + use crate::test_helpers::{base_seedream_5_lite_image_request, get_artcraft_client}; + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn test_text_to_image_seedream_5_lite() { + let client = get_artcraft_client(); + let request = GenerateImageRequest { + aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + image_batch_count: Some(1), + prompt: Some("a corgi walking through a cyberpunk city at night"), + ..base_seedream_5_lite_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. + } +} 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 59d28f63b..679dd8da8 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 @@ -4,6 +4,8 @@ pub mod generate_image_artcraft_flux_pro_1p1; pub mod generate_image_artcraft_flux_pro_1p1_ultra; pub mod generate_image_artcraft_gpt_image_1p5; pub mod generate_image_artcraft_nano_banana; +pub mod generate_image_artcraft_nano_banana_2; pub mod generate_image_artcraft_nano_banana_pro; pub mod generate_image_artcraft_seedream_4; pub mod generate_image_artcraft_seedream_4p5; +pub mod generate_image_artcraft_seedream_5_lite; 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 b4ae4492f..b5564e38d 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 @@ -14,9 +14,11 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::plan_generate_image_artcraft_flux_pro_1p1_ultra; 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_2::plan_generate_image_artcraft_nano_banana_2; 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; +use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::plan_generate_image_artcraft_seedream_5_lite; use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::plan_generate_image_fal_nano_banana_pro; pub struct GenerateImageRequest<'a> { @@ -79,6 +81,9 @@ impl<'a> GenerateImageRequest<'a> { CommonImageModel::NanaBanana => { plan_generate_image_artcraft_nano_banana(self).map(ImageGenerationPlan::ArtcraftNanaBanana) } + CommonImageModel::NanaBanana2 => { + plan_generate_image_artcraft_nano_banana_2(self).map(ImageGenerationPlan::ArtcraftNanaBanana2) + } CommonImageModel::NanaBananaPro => { plan_generate_image_artcraft_nano_banana_pro(self).map(ImageGenerationPlan::ArtcraftNanaBananaPro) } @@ -88,6 +93,9 @@ impl<'a> GenerateImageRequest<'a> { CommonImageModel::Seedream4p5 => { plan_generate_image_artcraft_seedream_4p5(self).map(ImageGenerationPlan::ArtcraftSeedream4p5) } + CommonImageModel::Seedream5Lite => { + plan_generate_image_artcraft_seedream_5_lite(self).map(ImageGenerationPlan::ArtcraftSeedream5Lite) + } }, Provider::Fal => match self.model { CommonImageModel::NanaBananaPro => { @@ -99,8 +107,10 @@ impl<'a> GenerateImageRequest<'a> { | CommonImageModel::FluxPro11Ultra | CommonImageModel::GptImage1p5 | CommonImageModel::NanaBanana + | CommonImageModel::NanaBanana2 | CommonImageModel::Seedream4 - | CommonImageModel::Seedream4p5 => { + | CommonImageModel::Seedream4p5 + | CommonImageModel::Seedream5Lite => { Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "provider", 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 e327250ad..78165afbc 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 @@ -6,9 +6,11 @@ use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraf use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_pro_1p1_ultra::estimate_image_cost_artcraft_flux_pro_1p1_ultra; 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_2::estimate_image_cost_artcraft_nano_banana_2; 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::artcraft::estimate_image_cost_artcraft_seedream_5_lite::estimate_image_cost_artcraft_seedream_5_lite; 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_flux_1_dev::execute_artcraft_flux_1_dev; use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_schnell::execute_artcraft_flux_1_schnell; @@ -16,9 +18,11 @@ use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_ use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_pro_1p1_ultra::execute_artcraft_flux_pro_1p1_ultra; 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_2::execute_artcraft_nano_banana_2; 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::artcraft::generate_image_artcraft_seedream_5_lite::execute_artcraft_seedream_5_lite; 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; @@ -28,9 +32,11 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::PlanArtcraftFluxPro11Ultra; 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_2::PlanArtcraftNanaBanana2; 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; +use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite; use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::PlanFalNanaBananaPro; #[derive(Debug)] @@ -41,9 +47,11 @@ pub enum ImageGenerationPlan<'a> { ArtcraftFluxPro11Ultra(PlanArtcraftFluxPro11Ultra<'a>), ArtcraftGptImage1p5(PlanArtcraftGptImage1p5<'a>), ArtcraftNanaBanana(PlanArtcraftNanaBanana<'a>), + ArtcraftNanaBanana2(PlanArtcraftNanaBanana2<'a>), ArtcraftNanaBananaPro(PlanArtcraftNanaBananaPro<'a>), ArtcraftSeedream4(PlanArtcraftSeedream4<'a>), ArtcraftSeedream4p5(PlanArtcraftSeedream4p5<'a>), + ArtcraftSeedream5Lite(PlanArtcraftSeedream5Lite<'a>), FalNanaBananaPro(PlanFalNanaBananaPro<'a>), } @@ -77,6 +85,10 @@ impl<'a> ImageGenerationPlan<'a> { let artcraft_client = client.get_artcraft_client_ref()?; execute_artcraft_nano_banana(plan, artcraft_client).await } + ImageGenerationPlan::ArtcraftNanaBanana2(plan) => { + let artcraft_client = client.get_artcraft_client_ref()?; + execute_artcraft_nano_banana_2(plan, artcraft_client).await + } ImageGenerationPlan::ArtcraftNanaBananaPro(plan) => { let artcraft_client = client.get_artcraft_client_ref()?; execute_artcraft_nano_banana_pro(plan, artcraft_client).await @@ -89,6 +101,10 @@ impl<'a> ImageGenerationPlan<'a> { let artcraft_client = client.get_artcraft_client_ref()?; execute_artcraft_seedream_4p5(plan, artcraft_client).await } + ImageGenerationPlan::ArtcraftSeedream5Lite(plan) => { + let artcraft_client = client.get_artcraft_client_ref()?; + execute_artcraft_seedream_5_lite(plan, artcraft_client).await + } ImageGenerationPlan::FalNanaBananaPro(plan) => { let fal_client = client.get_fal_client_ref()?; execute_fal_nano_banana_pro(plan, fal_client).await @@ -116,6 +132,9 @@ impl<'a> ImageGenerationPlan<'a> { ImageGenerationPlan::ArtcraftNanaBanana(plan) => { estimate_image_cost_artcraft_nano_banana(plan) } + ImageGenerationPlan::ArtcraftNanaBanana2(plan) => { + estimate_image_cost_artcraft_nano_banana_2(plan) + } ImageGenerationPlan::ArtcraftNanaBananaPro(plan) => { estimate_image_cost_artcraft_nano_banana_pro(plan) } @@ -125,6 +144,9 @@ impl<'a> ImageGenerationPlan<'a> { ImageGenerationPlan::ArtcraftSeedream4p5(plan) => { estimate_image_cost_artcraft_seedream_4p5(plan) } + ImageGenerationPlan::ArtcraftSeedream5Lite(plan) => { + estimate_image_cost_artcraft_seedream_5_lite(plan) + } ImageGenerationPlan::FalNanaBananaPro(plan) => { estimate_image_cost_fal_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 5129eb7c3..d34495771 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 @@ -4,6 +4,8 @@ pub mod plan_generate_image_artcraft_flux_pro_1p1; pub mod plan_generate_image_artcraft_flux_pro_1p1_ultra; 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_2; 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; +pub mod plan_generate_image_artcraft_seedream_5_lite; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana_2.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana_2.rs new file mode 100644 index 000000000..cfdd82b61 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana_2.rs @@ -0,0 +1,289 @@ +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::common_resolution::CommonResolution; +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_2_multi_function_image_gen::{ + NanaBanana2MultiFunctionImageGenAspectRatio, NanaBanana2MultiFunctionImageGenImageResolution, + NanaBanana2MultiFunctionImageGenNumImages, +}; +use tokens::tokens::media_files::MediaFileToken; + +#[derive(Debug, Clone)] +pub struct PlanArtcraftNanaBanana2<'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 resolution: Option, + pub num_images: NanaBanana2MultiFunctionImageGenNumImages, + pub idempotency_token: String, +} + +pub fn plan_generate_image_artcraft_nano_banana_2<'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 resolution = plan_resolution(request.resolution, strategy)?; + let num_images = plan_num_images(request.image_batch_count, strategy)?; + + Ok(PlanArtcraftNanaBanana2 { + prompt: request.prompt, + image_inputs, + aspect_ratio, + resolution, + 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 2 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 NanaBanana2MultiFunctionImageGenAspectRatio as Nb2Ar; + 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(Nb2Ar::Auto)) + } else { + // Auto not valid for text-to-image; fall back to square default + Ok(Some(Nb2Ar::OneByOne)) + } + } + + // Direct mappings + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(Nb2Ar::OneByOne)), + Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(Nb2Ar::FiveByFour)), + Some(CommonAspectRatio::WideFourByThree) => Ok(Some(Nb2Ar::FourByThree)), + Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(Nb2Ar::ThreeByTwo)), + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Nb2Ar::SixteenByNine)), + Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(Nb2Ar::TwentyOneByNine)), + Some(CommonAspectRatio::TallFourByFive) => Ok(Some(Nb2Ar::FourByFive)), + Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(Nb2Ar::ThreeByFour)), + Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(Nb2Ar::TwoByThree)), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Nb2Ar::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) -> NanaBanana2MultiFunctionImageGenAspectRatio { + use NanaBanana2MultiFunctionImageGenAspectRatio as Nb2Ar; + match aspect_ratio { + // TallNineByTwentyOne (0.43) — nearest is NineBySixteen (0.5625) + CommonAspectRatio::TallNineByTwentyOne => Nb2Ar::NineBySixteen, + _ => Nb2Ar::OneByOne, + } +} + +fn plan_resolution( + resolution: Option, + _strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use CommonResolution as R; + use NanaBanana2MultiFunctionImageGenImageResolution as Nb2Res; + match resolution { + None => Ok(None), + Some(R::OneK) => Ok(Some(Nb2Res::OneK)), + Some(R::TwoK) => Ok(Some(Nb2Res::TwoK)), + Some(R::FourK) => Ok(Some(Nb2Res::FourK)), + Some(R::ThreeK) => { + // No 3K option; default to 2K + Ok(Some(Nb2Res::TwoK)) + } + } +} + +// Nano Banana 2 supports 1, 2, 3, and 4 images. + +fn plan_num_images( + image_batch_count: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + use NanaBanana2MultiFunctionImageGenNumImages as Nb2N; + let count = image_batch_count.unwrap_or(1); + match count { + 0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)), + 1 => Ok(Nb2N::One), + 2 => Ok(Nb2N::Two), + 3 => Ok(Nb2N::Three), + 4 => Ok(Nb2N::Four), + _ => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "image_batch_count", + value: format!("{}", count), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Nb2N::Four), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Nb2N::Four), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::common_aspect_ratio::CommonAspectRatio; + use crate::api::common_resolution::CommonResolution; + 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_2_image_request; + use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::{ + NanaBanana2MultiFunctionImageGenAspectRatio as Nb2Ar, + NanaBanana2MultiFunctionImageGenImageResolution as Nb2Res, + NanaBanana2MultiFunctionImageGenNumImages as Nb2N, + }; + + // ── Aspect ratio ──────────────────────────────────────────────────────────── + + #[test] + fn aspect_ratio_none_is_none() { + let request = GenerateImageRequest { aspect_ratio: None, ..base_nano_banana_2_image_request() }; + let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") }; + 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_2_image_request() }; + let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") }; + assert!(matches!(plan.aspect_ratio, Some(Nb2Ar::OneByOne)), "expected OneByOne for {:?}", ar); + } + } + + #[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_2_image_request() + }; + let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") }; + assert!( + matches!(plan.aspect_ratio, Some(Nb2Ar::Auto)), + "expected Auto in edit mode for {:?}", auto_ar, + ); + } + } + + #[test] + fn aspect_ratio_auto_in_text_to_image_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_2_image_request() + }; + let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") }; + assert!( + matches!(plan.aspect_ratio, Some(Nb2Ar::OneByOne)), + "expected OneByOne fallback in text-to-image for {:?}", auto_ar, + ); + } + } + + // ── Resolution ────────────────────────────────────────────────────────────── + + #[test] + fn resolution_none_is_none() { + let request = GenerateImageRequest { resolution: None, ..base_nano_banana_2_image_request() }; + let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") }; + assert!(plan.resolution.is_none()); + } + + #[test] + fn resolution_direct_mappings() { + let cases = [ + (CommonResolution::OneK, Nb2Res::OneK), + (CommonResolution::TwoK, Nb2Res::TwoK), + (CommonResolution::FourK, Nb2Res::FourK), + ]; + for (common, expected) in cases { + let request = GenerateImageRequest { resolution: Some(common), ..base_nano_banana_2_image_request() }; + let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") }; + assert!( + matches!(plan.resolution, Some(r) if std::mem::discriminant(&r) == std::mem::discriminant(&expected)), + "expected {:?} for {:?}", expected, common, + ); + } + } + + #[test] + fn resolution_three_k_falls_back_to_two_k() { + let request = GenerateImageRequest { + resolution: Some(CommonResolution::ThreeK), + ..base_nano_banana_2_image_request() + }; + let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") }; + assert!(matches!(plan.resolution, Some(Nb2Res::TwoK))); + } + + // ── Num images ────────────────────────────────────────────────────────────── + + #[test] + fn num_images_direct_mapping() { + let cases = [ + (1, Nb2N::One), + (2, Nb2N::Two), + (3, Nb2N::Three), + (4, Nb2N::Four), + ]; + for (count, expected) in cases { + let request = GenerateImageRequest { image_batch_count: Some(count), ..base_nano_banana_2_image_request() }; + let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") }; + assert!( + std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected), + "expected {:?} for count {}", expected, count, + ); + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_5_lite.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_5_lite.rs new file mode 100644 index 000000000..f2ae1aa0d --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_5_lite.rs @@ -0,0 +1,211 @@ +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::bytedance_seedream_5_lite_multi_function_image_gen::{ + BytedanceSeedream5LiteMultiFunctionImageGenImageSize, + BytedanceSeedream5LiteMultiFunctionImageGenNumImages, +}; +use tokens::tokens::media_files::MediaFileToken; + +#[derive(Debug, Clone)] +pub struct PlanArtcraftSeedream5Lite<'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, + pub num_images: BytedanceSeedream5LiteMultiFunctionImageGenNumImages, + pub idempotency_token: String, +} + +pub fn plan_generate_image_artcraft_seedream_5_lite<'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 image_size = plan_image_size(request.aspect_ratio, is_edit_mode, strategy)?; + let num_images = plan_num_images(request.image_batch_count, strategy)?; + + Ok(PlanArtcraftSeedream5Lite { + prompt: request.prompt, + image_inputs, + image_size, + 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)) + } + } +} + +// Seedream 5 Lite supported image sizes: +// Square, SquareHd +// PortraitFourThree, PortraitSixteenNine +// LandscapeFourThree, LandscapeSixteenNine +// Auto2k, Auto3k (NB: 5 Lite max is auto_3K, unlike V4.5's auto_4K) +fn plan_image_size( + aspect_ratio: Option, + is_edit_mode: bool, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use BytedanceSeedream5LiteMultiFunctionImageGenImageSize as S; + match aspect_ratio { + None => Ok(None), + + // Auto: V5 has no bare Auto; fall back to Auto2k in edit mode, Square for text-to-image + Some(CommonAspectRatio::Auto) => { + if is_edit_mode { Ok(Some(S::Auto2k)) } else { Ok(Some(S::Square)) } + } + + Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)), + // 5 Lite max is auto_3K; map Auto4k down to Auto3k + Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto3k)), + + // Square + Some(CommonAspectRatio::Square) => Ok(Some(S::Square)), + Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)), + + // Wide — direct mappings + Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)), + Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)), + + // Wide — no direct equivalent, nearest is LandscapeSixteenNine + Some(unsupported @ CommonAspectRatio::WideFiveByFour) + | Some(unsupported @ CommonAspectRatio::WideThreeByTwo) + | Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade + | RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::LandscapeSixteenNine)), + }, + + // Tall — direct mappings + Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)), + Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)), + + // Tall — no direct equivalent, nearest is PortraitSixteenNine + Some(unsupported @ CommonAspectRatio::TallFourByFive) + | Some(unsupported @ CommonAspectRatio::TallTwoByThree) + | Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade + | RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::PortraitSixteenNine)), + }, + } +} + +fn plan_num_images( + image_batch_count: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + use BytedanceSeedream5LiteMultiFunctionImageGenNumImages as N; + let count = image_batch_count.unwrap_or(1); + match count { + 0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)), + 1 => Ok(N::One), + 2 => Ok(N::Two), + 3 => Ok(N::Three), + 4 => Ok(N::Four), + _ => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "image_batch_count", + value: format!("{}", count), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade + | RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(N::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_seedream_5_lite_image_request; + use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::{ + BytedanceSeedream5LiteMultiFunctionImageGenImageSize as S, + BytedanceSeedream5LiteMultiFunctionImageGenNumImages as N, + }; + + #[test] + fn image_size_none_is_none() { + let request = GenerateImageRequest { aspect_ratio: None, ..base_seedream_5_lite_image_request() }; + let ImageGenerationPlan::ArtcraftSeedream5Lite(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream5Lite") }; + assert!(plan.image_size.is_none()); + } + + #[test] + fn image_size_auto_in_edit_mode_yields_auto2k() { + let tokens = vec![]; + let request = GenerateImageRequest { + aspect_ratio: Some(CommonAspectRatio::Auto), + image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)), + ..base_seedream_5_lite_image_request() + }; + let ImageGenerationPlan::ArtcraftSeedream5Lite(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream5Lite") }; + assert!(matches!(plan.image_size, Some(S::Auto2k))); + } + + #[test] + fn image_size_auto_in_text_to_image_yields_square() { + let request = GenerateImageRequest { + aspect_ratio: Some(CommonAspectRatio::Auto), + image_inputs: None, + ..base_seedream_5_lite_image_request() + }; + let ImageGenerationPlan::ArtcraftSeedream5Lite(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream5Lite") }; + assert!(matches!(plan.image_size, Some(S::Square))); + } + + #[test] + fn auto4k_is_mapped_to_auto3k() { + // 5 Lite only supports up to auto_3K; auto_4K is downgraded + let request = GenerateImageRequest { + aspect_ratio: Some(CommonAspectRatio::Auto4k), + ..base_seedream_5_lite_image_request() + }; + let ImageGenerationPlan::ArtcraftSeedream5Lite(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream5Lite") }; + assert!(matches!(plan.image_size, Some(S::Auto3k))); + } + + #[test] + fn num_images_direct_mapping() { + let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)]; + for (count, expected) in cases { + let request = GenerateImageRequest { image_batch_count: Some(count), ..base_seedream_5_lite_image_request() }; + let ImageGenerationPlan::ArtcraftSeedream5Lite(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream5Lite") }; + assert!( + std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected), + "expected {:?} for count {}", expected, count, + ); + } + } +} 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 a2ba52c60..c105ae9e5 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs @@ -89,6 +89,36 @@ pub fn base_seedream_4p5_image_request() -> GenerateImageRequest<'static> { } } +pub fn base_seedream_5_lite_image_request() -> GenerateImageRequest<'static> { + GenerateImageRequest { + model: CommonImageModel::Seedream5Lite, + 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, + generation_mode_mismatch_strategy: None, + idempotency_token: None, + } +} + +pub fn base_nano_banana_2_image_request() -> GenerateImageRequest<'static> { + GenerateImageRequest { + model: CommonImageModel::NanaBanana2, + 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, + generation_mode_mismatch_strategy: None, + idempotency_token: None, + } +} + pub fn base_nano_banana_image_request() -> GenerateImageRequest<'static> { GenerateImageRequest { model: CommonImageModel::NanaBanana, diff --git a/crates/api_clients/fal_client/src/requests/http/image/http_nano_banana_2_edit_image.rs b/crates/api_clients/fal_client/src/requests/http/image/http_nano_banana_2_edit_image.rs new file mode 100644 index 000000000..46bdfe4c4 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/http/image/http_nano_banana_2_edit_image.rs @@ -0,0 +1,79 @@ +use fal::request::FalRequest; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct NanoBanana2EditImageInput { + pub prompt: String, + + pub image_urls: Vec, + + /// Eg. "auto", "16:9", "1:1", etc. + /// Options: auto, 21:9, 16:9, 3:2, 4:3, 5:4, 1:1, 4:5, 3:4, 2:3, 9:16 + /// Default: "auto" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Eg. "0.5K", "1K", "2K", "4K" + /// Default: "1K" + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// 1 - 4 + /// Default: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// "jpeg", "png", "webp" + /// Default: "png" + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub seed: Option, + + /// The safety tolerance level for content moderation. + /// 1 is the most strict (blocks most content), 6 is the least strict. + /// Default value: "4" + /// + /// Possible enum values: 1, 2, 3, 4, 5, 6 + #[serde(skip_serializing_if = "Option::is_none")] + pub safety_tolerance: Option, + + /// Experimental parameter to limit the number of generations from each round + /// of prompting to 1. Set to True to to disregard any instructions in the + /// prompt regarding the number of images to generate and ignore any + /// intermediate images generated by the model. This may affect generation + /// quality. + /// + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub limit_generations: Option, + + /// Enable web search for the image generation task. This will allow + /// the model to use the latest information from the web to generate + /// the image. + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_web_search: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NanoBanana2EditImageFile { + pub url: String, + //pub content_type: Option, + //pub file_name: Option, + //pub file_size: Option, + //pub width: Option, + //pub height: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NanoBanana2EditImageOutput { + pub images: Vec, + //pub description: String, +} + +pub fn nano_banana_2_edit_image( + params: NanoBanana2EditImageInput, +) -> FalRequest { + FalRequest::new("fal-ai/nano-banana-2/edit", params) +} diff --git a/crates/api_clients/fal_client/src/requests/http/image/http_nano_banana_2_text_to_image.rs b/crates/api_clients/fal_client/src/requests/http/image/http_nano_banana_2_text_to_image.rs new file mode 100644 index 000000000..dfa4c1de1 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/http/image/http_nano_banana_2_text_to_image.rs @@ -0,0 +1,77 @@ +use fal::request::FalRequest; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct NanoBanana2TextToImageInput { + pub prompt: String, + + /// Eg. "auto", "16:9", "1:1", etc. + /// Options: auto, 21:9, 16:9, 3:2, 4:3, 5:4, 1:1, 4:5, 3:4, 2:3, 9:16 + /// Default: "auto" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Eg. "0.5K", "1K", "2K", "4K" + /// Default: "1K" + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// 1 - 4 + /// Default: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// The safety tolerance level for content moderation. + /// 1 is the most strict (blocks most content), 6 is the least strict. + /// Default value: "4" + /// + /// Possible enum values: 1, 2, 3, 4, 5, 6 + #[serde(skip_serializing_if = "Option::is_none")] + pub safety_tolerance: Option, + + /// "jpeg", "png", "webp" + /// Default: "png" + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub seed: Option, + + /// Experimental parameter to limit the number of generations from each round + /// of prompting to 1. Set to True to to disregard any instructions in the + /// prompt regarding the number of images to generate and ignore any + /// intermediate images generated by the model. This may affect generation + /// quality. + /// + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub limit_generations: Option, + + /// Enable web search for the image generation task. This will allow + /// the model to use the latest information from the web to generate + /// the image. + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_web_search: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NanoBanana2ImageFile { + pub url: String, + //pub content_type: Option, + //pub file_name: Option, + //pub file_size: Option, + //pub width: Option, + //pub height: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NanoBanana2TextToImageOutput { + pub images: Vec, + //pub description: String, +} + +pub fn nano_banana_2_text_to_image( + params: NanoBanana2TextToImageInput, +) -> FalRequest { + FalRequest::new("fal-ai/nano-banana-2", params) +} diff --git a/crates/api_clients/fal_client/src/requests/http/image/http_seedream_5_edit_image.rs b/crates/api_clients/fal_client/src/requests/http/image/http_seedream_5_edit_image.rs new file mode 100644 index 000000000..3905ba423 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/http/image/http_seedream_5_edit_image.rs @@ -0,0 +1,49 @@ +use fal::request::FalRequest; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct SeedreamV5LiteEditImageInput { + pub prompt: String, + + /// Maximum 10 images. Excess images are ignored. + pub image_urls: Vec, + + /// Options: square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9, auto_2K, auto_3K + /// Default: "auto_2K" + #[serde(skip_serializing_if = "Option::is_none")] + pub image_size: Option, + + /// Default: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// Default: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub max_images: Option, + + /// Default: true + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_safety_checker: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SeedreamV5LiteEditImageFile { + pub url: String, + //pub content_type: Option, + //pub file_name: Option, + //pub file_size: Option, + //pub width: Option, + //pub height: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SeedreamV5LiteEditImageOutput { + pub images: Vec, + pub seed: u64, +} + +pub fn http_seedream_5_edit_image( + params: SeedreamV5LiteEditImageInput, +) -> FalRequest { + FalRequest::new("fal-ai/bytedance/seedream/v5/lite/edit", params) +} diff --git a/crates/api_clients/fal_client/src/requests/http/image/http_seedream_5_text_to_image.rs b/crates/api_clients/fal_client/src/requests/http/image/http_seedream_5_text_to_image.rs new file mode 100644 index 000000000..8061df6e3 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/http/image/http_seedream_5_text_to_image.rs @@ -0,0 +1,46 @@ +use fal::request::FalRequest; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct SeedreamV5LiteTextToImageInput { + pub prompt: String, + + /// Options: square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9, auto_2K, auto_3K + /// Default: "auto_2K" + #[serde(skip_serializing_if = "Option::is_none")] + pub image_size: Option, + + /// Default: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// Default: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub max_images: Option, + + /// Default: true + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_safety_checker: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SeedreamV5LiteImageFile { + pub url: String, + //pub content_type: Option, + //pub file_name: Option, + //pub file_size: Option, + //pub width: Option, + //pub height: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SeedreamV5LiteTextToImageOutput { + pub images: Vec, + pub seed: u64, +} + +pub fn http_seedream_5_text_to_image( + params: SeedreamV5LiteTextToImageInput, +) -> FalRequest { + FalRequest::new("fal-ai/bytedance/seedream/v5/lite/text-to-image", params) +} diff --git a/crates/api_clients/fal_client/src/requests/http/image/mod.rs b/crates/api_clients/fal_client/src/requests/http/image/mod.rs new file mode 100644 index 000000000..cdb5f0787 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/http/image/mod.rs @@ -0,0 +1,4 @@ +pub mod http_nano_banana_2_edit_image; +pub mod http_nano_banana_2_text_to_image; +pub mod http_seedream_5_edit_image; +pub mod http_seedream_5_text_to_image; diff --git a/crates/api_clients/fal_client/src/requests/http/mod.rs b/crates/api_clients/fal_client/src/requests/http/mod.rs new file mode 100644 index 000000000..14995d422 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/http/mod.rs @@ -0,0 +1 @@ +pub mod image; diff --git a/crates/api_clients/fal_client/src/requests/mod.rs b/crates/api_clients/fal_client/src/requests/mod.rs index e40922bb1..3035b4c88 100644 --- a/crates/api_clients/fal_client/src/requests/mod.rs +++ b/crates/api_clients/fal_client/src/requests/mod.rs @@ -1,3 +1,4 @@ +pub mod http; pub mod queue; -pub mod webhook; -pub mod traits; \ No newline at end of file +pub mod traits; +pub mod webhook; \ No newline at end of file diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_bytedance_seedream_v5_lite_edit_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_bytedance_seedream_v5_lite_edit_image_webhook.rs new file mode 100644 index 000000000..d67dbeb53 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_bytedance_seedream_v5_lite_edit_image_webhook.rs @@ -0,0 +1,158 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::http::image::http_seedream_5_edit_image::{http_seedream_5_edit_image, SeedreamV5LiteEditImageInput}; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueBytedanceSeedreamV5LiteEditImageArgs<'a, R: IntoUrl> { + // Request required + pub prompt: &'a str, + pub image_urls: Vec, + + // Optional args + pub num_images: Option, + pub max_images: Option, + pub image_size: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV5LiteEditImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV5LiteEditImageMaxImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV5LiteEditImageSize { + // Square + Square, + SquareHd, + // Tall + PortraitFourThree, + PortraitSixteenNine, + // Wide + LandscapeFourThree, + LandscapeSixteenNine, + // Auto + Auto2k, + Auto3k, // NB: v5 uses auto_3K instead of v4's auto_4K +} + + +impl FalRequestCostCalculator for EnqueueBytedanceSeedreamV5LiteEditImageArgs<'_, U> { + fn calculate_cost_in_cents(&self) -> UsdCents { + // TODO(bt): Verify actual pricing for Seedream v5 Lite on fal.ai. + let unit_cost = 4; + let cost = match self.num_images { + None => unit_cost, + Some(EnqueueBytedanceSeedreamV5LiteEditImageNumImages::One) => unit_cost, + Some(EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Two) => unit_cost * 2, + Some(EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Three) => unit_cost * 3, + Some(EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Four) => unit_cost * 4, + }; + cost as UsdCents + } +} + + +pub async fn enqueue_bytedance_seedream_v5_lite_edit_image_webhook( + args: EnqueueBytedanceSeedreamV5LiteEditImageArgs<'_, R> +) -> Result { + + let num_images = args.num_images + .map(|n| match n { + EnqueueBytedanceSeedreamV5LiteEditImageNumImages::One => 1, + EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Two => 2, + EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Three => 3, + EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Four => 4, + }); + + let max_images = args.max_images + .map(|n| match n { + EnqueueBytedanceSeedreamV5LiteEditImageMaxImages::One => 1, + EnqueueBytedanceSeedreamV5LiteEditImageMaxImages::Two => 2, + EnqueueBytedanceSeedreamV5LiteEditImageMaxImages::Three => 3, + EnqueueBytedanceSeedreamV5LiteEditImageMaxImages::Four => 4, + }); + + let image_size = args.image_size + .map(|s| match s { + EnqueueBytedanceSeedreamV5LiteEditImageSize::Square => "square", + EnqueueBytedanceSeedreamV5LiteEditImageSize::SquareHd => "square_hd", + EnqueueBytedanceSeedreamV5LiteEditImageSize::PortraitFourThree => "portrait_4_3", + EnqueueBytedanceSeedreamV5LiteEditImageSize::PortraitSixteenNine => "portrait_16_9", + EnqueueBytedanceSeedreamV5LiteEditImageSize::LandscapeFourThree => "landscape_4_3", + EnqueueBytedanceSeedreamV5LiteEditImageSize::LandscapeSixteenNine => "landscape_16_9", + EnqueueBytedanceSeedreamV5LiteEditImageSize::Auto2k => "auto_2K", + EnqueueBytedanceSeedreamV5LiteEditImageSize::Auto3k => "auto_3K", + }) + .map(|s| s.to_string()); + + let request = SeedreamV5LiteEditImageInput { + prompt: args.prompt.to_string(), + image_urls: args.image_urls, + // Optionals + num_images, + max_images, + image_size, + // Constants + enable_safety_checker: Some(false), + }; + + let result = http_seedream_5_edit_image(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::image::edit::enqueue_bytedance_seedream_v5_lite_edit_image_webhook::{enqueue_bytedance_seedream_v5_lite_edit_image_webhook, EnqueueBytedanceSeedreamV5LiteEditImageArgs, EnqueueBytedanceSeedreamV5LiteEditImageMaxImages, EnqueueBytedanceSeedreamV5LiteEditImageNumImages, EnqueueBytedanceSeedreamV5LiteEditImageSize}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::{GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL}; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueBytedanceSeedreamV5LiteEditImageArgs { + image_urls: vec![ + GHOST_IMAGE_URL.to_string(), + TREX_SKELETON_IMAGE_URL.to_string(), + ], + prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly", + num_images: Some(EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Two), + max_images: Some(EnqueueBytedanceSeedreamV5LiteEditImageMaxImages::Two), + image_size: Some(EnqueueBytedanceSeedreamV5LiteEditImageSize::Auto2k), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_bytedance_seedream_v5_lite_edit_image_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_nano_banana_2_edit_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_nano_banana_2_edit_image_webhook.rs new file mode 100644 index 000000000..42db36748 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_nano_banana_2_edit_image_webhook.rs @@ -0,0 +1,181 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::http::image::http_nano_banana_2_edit_image::{nano_banana_2_edit_image, NanoBanana2EditImageInput}; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueNanoBanana2EditImageArgs<'a, R: IntoUrl> { + // Request required + pub prompt: &'a str, + pub image_urls: Vec, + pub num_images: EnqueueNanoBanana2EditImageNumImages, + + // Optional args + pub resolution: Option, + pub aspect_ratio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueNanoBanana2EditImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueNanoBanana2EditImageResolution { + HalfK, // "0.5K" + OneK, // "1K" (default) + TwoK, // "2K" + FourK, // "4K" +} + +/// auto, 21:9, 16:9, 3:2, 4:3, 5:4, 1:1, 4:5, 3:4, 2:3, 9:16 +/// Default: "auto" +#[derive(Copy, Clone, Debug)] +pub enum EnqueueNanoBanana2EditImageAspectRatio { + // Automatic (default) + Auto, + // Square + OneByOne, + // Wide + FiveByFour, + FourByThree, + ThreeByTwo, + SixteenByNine, + TwentyOneByNine, + // Tall + FourByFive, + ThreeByFour, + TwoByThree, + NineBySixteen, +} + + +impl FalRequestCostCalculator for EnqueueNanoBanana2EditImageArgs<'_, U> { + fn calculate_cost_in_cents(&self) -> UsdCents { + // TODO(bt): Verify actual pricing for Nano Banana 2 on fal.ai. + // 4K outputs may be charged at double the standard rate. + let cost = match self.resolution { + None => 15, + Some(EnqueueNanoBanana2EditImageResolution::HalfK) => 8, + Some(EnqueueNanoBanana2EditImageResolution::OneK) => 15, + Some(EnqueueNanoBanana2EditImageResolution::TwoK) => 15, + Some(EnqueueNanoBanana2EditImageResolution::FourK) => 30, + }; + let cost = match self.num_images { + EnqueueNanoBanana2EditImageNumImages::One => cost, + EnqueueNanoBanana2EditImageNumImages::Two => cost * 2, + EnqueueNanoBanana2EditImageNumImages::Three => cost * 3, + EnqueueNanoBanana2EditImageNumImages::Four => cost * 4, + }; + cost as UsdCents + } +} + + +pub async fn enqueue_nano_banana_2_edit_image_webhook( + args: EnqueueNanoBanana2EditImageArgs<'_, R> +) -> Result { + + let num_images = match args.num_images { + EnqueueNanoBanana2EditImageNumImages::One => 1, + EnqueueNanoBanana2EditImageNumImages::Two => 2, + EnqueueNanoBanana2EditImageNumImages::Three => 3, + EnqueueNanoBanana2EditImageNumImages::Four => 4, + }; + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueNanoBanana2EditImageResolution::HalfK => "0.5K", + EnqueueNanoBanana2EditImageResolution::OneK => "1K", + EnqueueNanoBanana2EditImageResolution::TwoK => "2K", + EnqueueNanoBanana2EditImageResolution::FourK => "4K", + }) + .map(|r| r.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|ar| match ar { + // Auto + EnqueueNanoBanana2EditImageAspectRatio::Auto => "auto", + // Square + EnqueueNanoBanana2EditImageAspectRatio::OneByOne => "1:1", + // Wide + EnqueueNanoBanana2EditImageAspectRatio::FiveByFour => "5:4", + EnqueueNanoBanana2EditImageAspectRatio::FourByThree => "4:3", + EnqueueNanoBanana2EditImageAspectRatio::ThreeByTwo => "3:2", + EnqueueNanoBanana2EditImageAspectRatio::SixteenByNine => "16:9", + EnqueueNanoBanana2EditImageAspectRatio::TwentyOneByNine => "21:9", + // Tall + EnqueueNanoBanana2EditImageAspectRatio::FourByFive => "4:5", + EnqueueNanoBanana2EditImageAspectRatio::ThreeByFour => "3:4", + EnqueueNanoBanana2EditImageAspectRatio::TwoByThree => "2:3", + EnqueueNanoBanana2EditImageAspectRatio::NineBySixteen => "9:16", + }) + .map(|ar| ar.to_string()); + + let request = NanoBanana2EditImageInput { + prompt: args.prompt.to_string(), + image_urls: args.image_urls, + num_images: Some(num_images), + // Optionals + aspect_ratio, + resolution, + // Constants + output_format: Some("png".to_string()), + safety_tolerance: Some("6".to_string()), + limit_generations: None, + enable_web_search: None, + seed: None, + }; + + let result = nano_banana_2_edit_image(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::image::edit::enqueue_nano_banana_2_edit_image_webhook::{enqueue_nano_banana_2_edit_image_webhook, EnqueueNanoBanana2EditImageArgs, EnqueueNanoBanana2EditImageAspectRatio, EnqueueNanoBanana2EditImageNumImages, EnqueueNanoBanana2EditImageResolution}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL}; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueNanoBanana2EditImageArgs { + image_urls: vec![ + GHOST_IMAGE_URL.to_string(), + TREX_SKELETON_IMAGE_URL.to_string(), + ERNEST_SCARED_STUPID_IMAGE_URL.to_string(), + ], + prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly", + num_images: EnqueueNanoBanana2EditImageNumImages::Two, + aspect_ratio: Some(EnqueueNanoBanana2EditImageAspectRatio::SixteenByNine), + resolution: Some(EnqueueNanoBanana2EditImageResolution::TwoK), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_nano_banana_2_edit_image_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/edit/mod.rs b/crates/api_clients/fal_client/src/requests/webhook/image/edit/mod.rs index e58f936c1..902837f20 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/image/edit/mod.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/image/edit/mod.rs @@ -1,5 +1,7 @@ pub mod enqueue_bytedance_seedream_v4_edit_image_webhook; pub mod enqueue_bytedance_seedream_v4p5_edit_image_webhook; +pub mod enqueue_bytedance_seedream_v5_lite_edit_image_webhook; +pub mod enqueue_nano_banana_2_edit_image_webhook; pub mod enqueue_flux_pro_kontext_max_edit_webhook; pub mod enqueue_gemini_25_flash_edit_webhook; pub mod enqueue_gpt_image_1_edit_image_webhook; diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_bytedance_seedream_v5_lite_text_to_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_bytedance_seedream_v5_lite_text_to_image_webhook.rs new file mode 100644 index 000000000..85f6b7cdf --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_bytedance_seedream_v5_lite_text_to_image_webhook.rs @@ -0,0 +1,151 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::http::image::http_seedream_5_text_to_image::{http_seedream_5_text_to_image, SeedreamV5LiteTextToImageInput}; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueBytedanceSeedreamV5LiteTextToImageArgs<'a, R: IntoUrl> { + // Request required + pub prompt: &'a str, + + // Optional args + pub num_images: Option, + pub max_images: Option, + pub image_size: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV5LiteTextToImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV5LiteTextToImageSize { + // Square + Square, + SquareHd, + // Tall + PortraitFourThree, + PortraitSixteenNine, + // Wide + LandscapeFourThree, + LandscapeSixteenNine, + // Auto + Auto2k, + Auto3k, // NB: v5 uses auto_3K instead of v4's auto_4K +} + + +impl FalRequestCostCalculator for EnqueueBytedanceSeedreamV5LiteTextToImageArgs<'_, U> { + fn calculate_cost_in_cents(&self) -> UsdCents { + // TODO(bt): Verify actual pricing for Seedream v5 Lite on fal.ai. + let unit_cost = 4; + let cost = match self.num_images { + None => unit_cost, + Some(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::One) => unit_cost, + Some(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Two) => unit_cost * 2, + Some(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Three) => unit_cost * 3, + Some(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Four) => unit_cost * 4, + }; + cost as UsdCents + } +} + + +pub async fn enqueue_bytedance_seedream_v5_lite_text_to_image_webhook( + args: EnqueueBytedanceSeedreamV5LiteTextToImageArgs<'_, R> +) -> Result { + + let num_images = args.num_images + .map(|n| match n { + EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::One => 1, + EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Two => 2, + EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Three => 3, + EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Four => 4, + }); + + let max_images = args.max_images + .map(|n| match n { + EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages::One => 1, + EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages::Two => 2, + EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages::Three => 3, + EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages::Four => 4, + }); + + let image_size = args.image_size + .map(|s| match s { + EnqueueBytedanceSeedreamV5LiteTextToImageSize::Square => "square", + EnqueueBytedanceSeedreamV5LiteTextToImageSize::SquareHd => "square_hd", + EnqueueBytedanceSeedreamV5LiteTextToImageSize::PortraitFourThree => "portrait_4_3", + EnqueueBytedanceSeedreamV5LiteTextToImageSize::PortraitSixteenNine => "portrait_16_9", + EnqueueBytedanceSeedreamV5LiteTextToImageSize::LandscapeFourThree => "landscape_4_3", + EnqueueBytedanceSeedreamV5LiteTextToImageSize::LandscapeSixteenNine => "landscape_16_9", + EnqueueBytedanceSeedreamV5LiteTextToImageSize::Auto2k => "auto_2K", + EnqueueBytedanceSeedreamV5LiteTextToImageSize::Auto3k => "auto_3K", + }) + .map(|s| s.to_string()); + + let request = SeedreamV5LiteTextToImageInput { + prompt: args.prompt.to_string(), + // Optionals + num_images, + max_images, + image_size, + // Constants + enable_safety_checker: Some(false), + }; + + let result = http_seedream_5_text_to_image(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::{enqueue_bytedance_seedream_v5_lite_text_to_image_webhook, EnqueueBytedanceSeedreamV5LiteTextToImageArgs, EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages, EnqueueBytedanceSeedreamV5LiteTextToImageNumImages, EnqueueBytedanceSeedreamV5LiteTextToImageSize}; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueBytedanceSeedreamV5LiteTextToImageArgs { + prompt: "an anime girl is riding a t-rex in the forest", + num_images: Some(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Two), + max_images: Some(EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages::Two), + image_size: Some(EnqueueBytedanceSeedreamV5LiteTextToImageSize::LandscapeSixteenNine), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_bytedance_seedream_v5_lite_text_to_image_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_nano_banana_2_text_to_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_nano_banana_2_text_to_image_webhook.rs new file mode 100644 index 000000000..1f0688742 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_nano_banana_2_text_to_image_webhook.rs @@ -0,0 +1,173 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::http::image::http_nano_banana_2_text_to_image::{nano_banana_2_text_to_image, NanoBanana2TextToImageInput}; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueNanoBanana2TextToImageArgs<'a, R: IntoUrl> { + // Request required + pub prompt: &'a str, + pub num_images: EnqueueNanoBanana2TextToImageNumImages, + + // Optional args + pub resolution: Option, + pub aspect_ratio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueNanoBanana2TextToImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueNanoBanana2TextToImageResolution { + HalfK, // "0.5K" + OneK, // "1K" (default) + TwoK, // "2K" + FourK, // "4K" +} + +/// auto, 21:9, 16:9, 3:2, 4:3, 5:4, 1:1, 4:5, 3:4, 2:3, 9:16 +/// Default: "auto" +#[derive(Copy, Clone, Debug)] +pub enum EnqueueNanoBanana2TextToImageAspectRatio { + // Automatic (default) + Auto, + // Square + OneByOne, + // Wide + FiveByFour, + FourByThree, + ThreeByTwo, + SixteenByNine, + TwentyOneByNine, + // Tall + FourByFive, + ThreeByFour, + TwoByThree, + NineBySixteen, +} + + +impl FalRequestCostCalculator for EnqueueNanoBanana2TextToImageArgs<'_, U> { + fn calculate_cost_in_cents(&self) -> UsdCents { + // TODO(bt): Verify actual pricing for Nano Banana 2 on fal.ai. + // 4K outputs may be charged at double the standard rate. + let cost = match self.resolution { + None => 15, + Some(EnqueueNanoBanana2TextToImageResolution::HalfK) => 8, + Some(EnqueueNanoBanana2TextToImageResolution::OneK) => 15, + Some(EnqueueNanoBanana2TextToImageResolution::TwoK) => 15, + Some(EnqueueNanoBanana2TextToImageResolution::FourK) => 30, + }; + let cost = match self.num_images { + EnqueueNanoBanana2TextToImageNumImages::One => cost, + EnqueueNanoBanana2TextToImageNumImages::Two => cost * 2, + EnqueueNanoBanana2TextToImageNumImages::Three => cost * 3, + EnqueueNanoBanana2TextToImageNumImages::Four => cost * 4, + }; + cost as UsdCents + } +} + + +pub async fn enqueue_nano_banana_2_text_to_image_webhook( + args: EnqueueNanoBanana2TextToImageArgs<'_, R> +) -> Result { + + let num_images = match args.num_images { + EnqueueNanoBanana2TextToImageNumImages::One => 1, + EnqueueNanoBanana2TextToImageNumImages::Two => 2, + EnqueueNanoBanana2TextToImageNumImages::Three => 3, + EnqueueNanoBanana2TextToImageNumImages::Four => 4, + }; + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueNanoBanana2TextToImageResolution::HalfK => "0.5K", + EnqueueNanoBanana2TextToImageResolution::OneK => "1K", + EnqueueNanoBanana2TextToImageResolution::TwoK => "2K", + EnqueueNanoBanana2TextToImageResolution::FourK => "4K", + }) + .map(|r| r.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|ar| match ar { + // Auto + EnqueueNanoBanana2TextToImageAspectRatio::Auto => "auto", + // Square + EnqueueNanoBanana2TextToImageAspectRatio::OneByOne => "1:1", + // Wide + EnqueueNanoBanana2TextToImageAspectRatio::FiveByFour => "5:4", + EnqueueNanoBanana2TextToImageAspectRatio::FourByThree => "4:3", + EnqueueNanoBanana2TextToImageAspectRatio::ThreeByTwo => "3:2", + EnqueueNanoBanana2TextToImageAspectRatio::SixteenByNine => "16:9", + EnqueueNanoBanana2TextToImageAspectRatio::TwentyOneByNine => "21:9", + // Tall + EnqueueNanoBanana2TextToImageAspectRatio::FourByFive => "4:5", + EnqueueNanoBanana2TextToImageAspectRatio::ThreeByFour => "3:4", + EnqueueNanoBanana2TextToImageAspectRatio::TwoByThree => "2:3", + EnqueueNanoBanana2TextToImageAspectRatio::NineBySixteen => "9:16", + }) + .map(|ar| ar.to_string()); + + let request = NanoBanana2TextToImageInput { + prompt: args.prompt.to_string(), + num_images: Some(num_images), + // Optionals + aspect_ratio, + resolution, + // Constants + output_format: Some("png".to_string()), + limit_generations: None, + safety_tolerance: Some("6".to_string()), + enable_web_search: None, + seed: None, + }; + + let result = nano_banana_2_text_to_image(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::image::text::enqueue_nano_banana_2_text_to_image_webhook::{enqueue_nano_banana_2_text_to_image_webhook, EnqueueNanoBanana2TextToImageArgs, EnqueueNanoBanana2TextToImageAspectRatio, EnqueueNanoBanana2TextToImageNumImages, EnqueueNanoBanana2TextToImageResolution}; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueNanoBanana2TextToImageArgs { + prompt: "an anime girl riding on the back of a t-rex", + num_images: EnqueueNanoBanana2TextToImageNumImages::One, + aspect_ratio: Some(EnqueueNanoBanana2TextToImageAspectRatio::SixteenByNine), + resolution: Some(EnqueueNanoBanana2TextToImageResolution::TwoK), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_nano_banana_2_text_to_image_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/text/mod.rs b/crates/api_clients/fal_client/src/requests/webhook/image/text/mod.rs index df81dcec4..878203ddb 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/image/text/mod.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/image/text/mod.rs @@ -1,5 +1,7 @@ pub mod enqueue_bytedance_seedream_v4_text_to_image_webhook; pub mod enqueue_bytedance_seedream_v4p5_text_to_image_webhook; +pub mod enqueue_bytedance_seedream_v5_lite_text_to_image_webhook; +pub mod enqueue_nano_banana_2_text_to_image_webhook; pub mod enqueue_flux_1_dev_text_to_image_webhook; pub mod enqueue_flux_1_schnell_text_to_image_webhook; pub mod enqueue_flux_pro_11_text_to_image_webhook; 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 dbbb34958..64944316d 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 @@ -25,10 +25,12 @@ pub async fn handle_image_edit_artcraft( ImageEditModel::Gemini25Flash | ImageEditModel::NanoBanana => { handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBanana, GenerationModel::NanoBanana).await }, + ImageEditModel::NanoBanana2 => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBanana2, GenerationModel::NanoBanana2).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_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, + ImageEditModel::Seedream5Lite => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream5Lite, GenerationModel::Seedream5Lite).await, } } diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_edit_image_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_edit_image_command.rs index 4dd389f86..84041fba3 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_edit_image_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_edit_image_command.rs @@ -60,24 +60,30 @@ pub enum ImageEditModel { #[serde(rename = "nano_banana")] NanoBanana, + #[serde(rename = "nano_banana_2")] + NanoBanana2, + #[serde(rename = "nano_banana_pro")] NanoBananaPro, #[serde(rename = "gpt_image_1")] GptImage1, - + #[serde(rename = "gpt_image_1p5")] GptImage1p5, - + #[serde(rename = "seedream_4")] Seedream4, - + #[serde(rename = "seedream_4p5")] Seedream4p5, - + + #[serde(rename = "seedream_5_lite")] + Seedream5Lite, + // #[serde(rename = "qwen")] // Qwen, -// +// // #[serde(rename = "seededit_3")] // SeedEdit3, } diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/image_edit_models.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/image_edit_models.rs index dba05ba14..bf2948452 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/image_edit_models.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/image_edit_models.rs @@ -6,10 +6,12 @@ pub fn image_edit_model_to_model_type(model: ImageEditModel) -> ModelType { ImageEditModel::FluxProKontextMax => ModelType::FluxProKontextMax, ImageEditModel::Gemini25Flash => ModelType::NanoBanana, ImageEditModel::NanoBanana => ModelType::NanoBanana, + ImageEditModel::NanoBanana2 => ModelType::NanoBanana2, ImageEditModel::NanoBananaPro => ModelType::NanoBananaPro, ImageEditModel::GptImage1 => ModelType::GptImage1, ImageEditModel::GptImage1p5 => ModelType::GptImage1p5, ImageEditModel::Seedream4 => ModelType::Seedream4, ImageEditModel::Seedream4p5 => ModelType::Seedream4p5, + ImageEditModel::Seedream5Lite => ModelType::Seedream5Lite, } } 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 5a382e58f..8c680f1e0 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 @@ -40,8 +40,10 @@ pub async fn handle_text_to_image_artcraft( TextToImageModel::Gemini25Flash | TextToImageModel::NanoBanana => { handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBanana, GenerationModel::NanoBanana).await }, + TextToImageModel::NanoBanana2 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBanana2, GenerationModel::NanoBanana2).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, TextToImageModel::Seedream4p5 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream4p5, GenerationModel::Seedream4p5).await, + TextToImageModel::Seedream5Lite => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream5Lite, GenerationModel::Seedream5Lite).await, } } diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/enqueue_text_to_image_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/enqueue_text_to_image_command.rs index 056de965b..5dd79fce6 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/enqueue_text_to_image_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/enqueue_text_to_image_command.rs @@ -65,12 +65,16 @@ pub enum TextToImageModel { Gemini25Flash, #[serde(rename = "nano_banana")] NanoBanana, + #[serde(rename = "nano_banana_2")] + NanoBanana2, #[serde(rename = "nano_banana_pro")] NanoBananaPro, #[serde(rename = "seedream_4")] Seedream4, #[serde(rename = "seedream_4p5")] Seedream4p5, + #[serde(rename = "seedream_5_lite")] + Seedream5Lite, // Generic Midjourney model, version unknown. #[serde(rename = "midjourney")] diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/text_to_image_models.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/text_to_image_models.rs index a48ce3ac4..4f64d599d 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/text_to_image_models.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/text_to_image_models.rs @@ -13,9 +13,11 @@ pub fn text_to_image_model_to_model_type(model: TextToImageModel) -> ModelType { TextToImageModel::GptImage1p5 => ModelType::GptImage1p5, TextToImageModel::Gemini25Flash => ModelType::NanoBanana, TextToImageModel::NanoBanana => ModelType::NanoBanana, + TextToImageModel::NanoBanana2 => ModelType::NanoBanana2, TextToImageModel::NanoBananaPro => ModelType::NanoBananaPro, TextToImageModel::Seedream4 => ModelType::Seedream4, TextToImageModel::Seedream4p5 => ModelType::Seedream4p5, + TextToImageModel::Seedream5Lite => ModelType::Seedream5Lite, TextToImageModel::Midjourney => ModelType::Midjourney, } } diff --git a/crates/desktop/artcraft/src/core/events/generation_events/common.rs b/crates/desktop/artcraft/src/core/events/generation_events/common.rs index ead595c69..67de7f582 100644 --- a/crates/desktop/artcraft/src/core/events/generation_events/common.rs +++ b/crates/desktop/artcraft/src/core/events/generation_events/common.rs @@ -39,12 +39,16 @@ pub enum GenerationModel { Gemini25Flash, #[serde(rename = "nano_banana")] NanoBanana, + #[serde(rename = "nano_banana_2")] + NanoBanana2, #[serde(rename = "nano_banana_pro")] NanoBananaPro, #[serde(rename = "seedream_4")] Seedream4, #[serde(rename = "seedream_4p5")] Seedream4p5, + #[serde(rename = "seedream_5_lite")] + Seedream5Lite, #[serde(rename = "hunyuan_3d_2_0")] Hunyuan3d2_0, diff --git a/crates/schema/public/enums/src/common/generation/common_image_model.rs b/crates/schema/public/enums/src/common/generation/common_image_model.rs index a9aa9345d..52d21cc68 100644 --- a/crates/schema/public/enums/src/common/generation/common_image_model.rs +++ b/crates/schema/public/enums/src/common/generation/common_image_model.rs @@ -17,10 +17,14 @@ pub enum CommonImageModel { GptImage1p5, #[serde(rename = "nano_banana")] NanaBanana, + #[serde(rename = "nano_banana_2")] + NanaBanana2, #[serde(rename = "nano_banana_pro")] NanaBananaPro, #[serde(rename = "seedream_4")] Seedream4, #[serde(rename = "seedream_4p5")] Seedream4p5, + #[serde(rename = "seedream_5_lite")] + Seedream5Lite, } diff --git a/crates/schema/public/enums/src/common/model_type.rs b/crates/schema/public/enums/src/common/model_type.rs index 420c57e50..366bc03b2 100644 --- a/crates/schema/public/enums/src/common/model_type.rs +++ b/crates/schema/public/enums/src/common/model_type.rs @@ -45,12 +45,16 @@ pub enum ModelType { Gemini25Flash, #[serde(rename = "nano_banana")] NanoBanana, + #[serde(rename = "nano_banana_2")] + NanoBanana2, #[serde(rename = "nano_banana_pro")] NanoBananaPro, #[serde(rename = "seedream_4")] Seedream4, #[serde(rename = "seedream_4p5")] Seedream4p5, + #[serde(rename = "seedream_5_lite")] + Seedream5Lite, /// Midjourney without distinguishing a model type or version #[serde(rename = "midjourney")] @@ -144,9 +148,11 @@ impl ModelType { Self::Qwen => "qwen", Self::Gemini25Flash => "gemini_25_flash", Self::NanoBanana => "nano_banana", + Self::NanoBanana2 => "nano_banana_2", Self::NanoBananaPro => "nano_banana_pro", Self::Seedream4 => "seedream_4", Self::Seedream4p5 => "seedream_4p5", + Self::Seedream5Lite => "seedream_5_lite", Self::Midjourney => "midjourney", Self::MidjourneyV6 => "midjourney_v6", Self::MidjourneyV6p1 => "midjourney_v6p1", @@ -199,9 +205,11 @@ impl ModelType { "qwen" => Ok(Self::Qwen), "gemini_25_flash" => Ok(Self::Gemini25Flash), "nano_banana" => Ok(Self::NanoBanana), + "nano_banana_2" => Ok(Self::NanoBanana2), "nano_banana_pro" => Ok(Self::NanoBananaPro), "seedream_4" => Ok(Self::Seedream4), "seedream_4p5" => Ok(Self::Seedream4p5), + "seedream_5_lite" => Ok(Self::Seedream5Lite), "midjourney" => Ok(Self::Midjourney), "midjourney_v6" => Ok(Self::MidjourneyV6), "midjourney_v6p1" => Ok(Self::MidjourneyV6p1), @@ -258,9 +266,11 @@ impl ModelType { Self::Qwen, Self::Gemini25Flash, Self::NanoBanana, + Self::NanoBanana2, Self::NanoBananaPro, Self::Seedream4, Self::Seedream4p5, + Self::Seedream5Lite, Self::Midjourney, Self::MidjourneyV6, Self::MidjourneyV6p1, @@ -322,9 +332,11 @@ mod tests { assert_serialization(ModelType::Qwen, "qwen"); assert_serialization(ModelType::Gemini25Flash, "gemini_25_flash"); assert_serialization(ModelType::NanoBanana, "nano_banana"); + assert_serialization(ModelType::NanoBanana2, "nano_banana_2"); assert_serialization(ModelType::NanoBananaPro, "nano_banana_pro"); assert_serialization(ModelType::Seedream4, "seedream_4"); assert_serialization(ModelType::Seedream4p5, "seedream_4p5"); + assert_serialization(ModelType::Seedream5Lite, "seedream_5_lite"); assert_serialization(ModelType::Midjourney, "midjourney"); assert_serialization(ModelType::MidjourneyV6, "midjourney_v6"); assert_serialization(ModelType::MidjourneyV6p1, "midjourney_v6p1"); @@ -374,9 +386,11 @@ mod tests { assert_eq!(ModelType::Qwen.to_str(), "qwen"); assert_eq!(ModelType::Gemini25Flash.to_str(), "gemini_25_flash"); assert_eq!(ModelType::NanoBanana.to_str(), "nano_banana"); + assert_eq!(ModelType::NanoBanana2.to_str(), "nano_banana_2"); assert_eq!(ModelType::NanoBananaPro.to_str(), "nano_banana_pro"); assert_eq!(ModelType::Seedream4.to_str(), "seedream_4"); assert_eq!(ModelType::Seedream4p5.to_str(), "seedream_4p5"); + assert_eq!(ModelType::Seedream5Lite.to_str(), "seedream_5_lite"); assert_eq!(ModelType::Midjourney.to_str(), "midjourney"); assert_eq!(ModelType::MidjourneyV6.to_str(), "midjourney_v6"); assert_eq!(ModelType::MidjourneyV6p1.to_str(), "midjourney_v6p1"); @@ -428,9 +442,11 @@ mod tests { assert_eq!(ModelType::from_str("qwen").unwrap(), ModelType::Qwen); assert_eq!(ModelType::from_str("gemini_25_flash").unwrap(), ModelType::Gemini25Flash); assert_eq!(ModelType::from_str("nano_banana").unwrap(), ModelType::NanoBanana); + assert_eq!(ModelType::from_str("nano_banana_2").unwrap(), ModelType::NanoBanana2); assert_eq!(ModelType::from_str("nano_banana_pro").unwrap(), ModelType::NanoBananaPro); assert_eq!(ModelType::from_str("seedream_4").unwrap(), ModelType::Seedream4); assert_eq!(ModelType::from_str("seedream_4p5").unwrap(), ModelType::Seedream4p5); + assert_eq!(ModelType::from_str("seedream_5_lite").unwrap(), ModelType::Seedream5Lite); assert_eq!(ModelType::from_str("midjourney").unwrap(), ModelType::Midjourney); assert_eq!(ModelType::from_str("midjourney_v6").unwrap(), ModelType::MidjourneyV6); assert_eq!(ModelType::from_str("midjourney_v6p1").unwrap(), ModelType::MidjourneyV6p1); @@ -466,7 +482,7 @@ mod tests { #[test] fn all_variants() { let mut variants = ModelType::all_variants(); - assert_eq!(variants.len(), 45); + assert_eq!(variants.len(), 47); // Image models assert_eq!(variants.pop_first(), Some(ModelType::Flux1Dev)); assert_eq!(variants.pop_first(), Some(ModelType::Flux1Schnell)); @@ -483,9 +499,11 @@ mod tests { assert_eq!(variants.pop_first(), Some(ModelType::Qwen)); assert_eq!(variants.pop_first(), Some(ModelType::Gemini25Flash)); assert_eq!(variants.pop_first(), Some(ModelType::NanoBanana)); + assert_eq!(variants.pop_first(), Some(ModelType::NanoBanana2)); assert_eq!(variants.pop_first(), Some(ModelType::NanoBananaPro)); assert_eq!(variants.pop_first(), Some(ModelType::Seedream4)); assert_eq!(variants.pop_first(), Some(ModelType::Seedream4p5)); + assert_eq!(variants.pop_first(), Some(ModelType::Seedream5Lite)); assert_eq!(variants.pop_first(), Some(ModelType::Midjourney)); assert_eq!(variants.pop_first(), Some(ModelType::MidjourneyV6)); assert_eq!(variants.pop_first(), Some(ModelType::MidjourneyV6p1)); diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/image/estimate_image_cost_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/image/estimate_image_cost_handler.rs index bc68ae005..5b7ef7e00 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/image/estimate_image_cost_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/image/estimate_image_cost_handler.rs @@ -11,6 +11,7 @@ use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterAspect use artcraft_router::api::common_image_model::CommonImageModel as RouterImageModel; use artcraft_router::api::common_resolution::CommonResolution as RouterResolution; use artcraft_router::api::provider::Provider as RouterProvider; +use artcraft_router::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy; use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest; use enums::common::generation::common_aspect_ratio::CommonAspectRatio; @@ -40,13 +41,13 @@ pub async fn estimate_image_cost_handler( let router_request = GenerateImageRequest { model: router_model, provider: router_provider, - prompt: None, - image_inputs: None, + prompt: None, // NB: Prompt is immaterial to cost estimation + image_inputs: None, // TODO: Only some models charge for this - we'll need to add later. resolution: router_resolution, aspect_ratio: router_aspect_ratio, image_batch_count: request.image_batch_count, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade, - generation_mode_mismatch_strategy: None, + generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway), idempotency_token: None, }; @@ -127,9 +128,11 @@ fn map_image_model(model: CommonImageModel) -> Result RouterImageModel::FluxPro11Ultra, CommonImageModel::GptImage1p5 => RouterImageModel::GptImage1p5, CommonImageModel::NanaBanana => RouterImageModel::NanaBanana, + CommonImageModel::NanaBanana2 => RouterImageModel::NanaBanana2, CommonImageModel::NanaBananaPro => RouterImageModel::NanaBananaPro, CommonImageModel::Seedream4 => RouterImageModel::Seedream4, CommonImageModel::Seedream4p5 => RouterImageModel::Seedream4p5, + CommonImageModel::Seedream5Lite => RouterImageModel::Seedream5Lite, }; Ok(router_model) } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/multi_function/bytedance_seedream_5_lite_multi_function_image_gen_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/multi_function/bytedance_seedream_5_lite_multi_function_image_gen_handler.rs new file mode 100644 index 000000000..cedc9fa38 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/multi_function/bytedance_seedream_5_lite_multi_function_image_gen_handler.rs @@ -0,0 +1,301 @@ +use std::sync::Arc; + +use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::state::server_state::ServerState; +use crate::util::lookup::lookup_image_urls_as_optional_list::lookup_image_urls_as_optional_list; +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; +use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::{BytedanceSeedream5LiteMultiFunctionImageGenImageSize, BytedanceSeedream5LiteMultiFunctionImageGenNumImages, BytedanceSeedream5LiteMultiFunctionImageGenRequest, BytedanceSeedream5LiteMultiFunctionImageGenResponse}; +use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType; +use enums::by_table::prompts::prompt_type::PromptType; +use enums::common::generation_provider::GenerationProvider; +use enums::common::model_type::ModelType; +use enums::common::visibility::Visibility; +use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator; +use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v5_lite_edit_image_webhook::{enqueue_bytedance_seedream_v5_lite_edit_image_webhook, EnqueueBytedanceSeedreamV5LiteEditImageArgs, EnqueueBytedanceSeedreamV5LiteEditImageNumImages, EnqueueBytedanceSeedreamV5LiteEditImageSize}; +use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::{enqueue_bytedance_seedream_v5_lite_text_to_image_webhook, EnqueueBytedanceSeedreamV5LiteTextToImageArgs, EnqueueBytedanceSeedreamV5LiteTextToImageNumImages, EnqueueBytedanceSeedreamV5LiteTextToImageSize}; +use http_server_common::request::get_request_ip::get_request_ip; +use log::{error, info, warn}; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue_with_apriori_job_token::{insert_generic_inference_job_for_fal_queue_with_apriori_job_token, InsertGenericInferenceForFalWithAprioriJobTokenArgs}; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem}; +use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs}; +use sqlx::Acquire; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; + +/// Bytedance Seedream 5 Lite Multi-Function (generate + edit) +#[utoipa::path( + post, + tag = "Generate Images (Multi-Function)", + path = "/v1/generate/image/multi_function/bytedance_seedream_5_lite", + responses( + (status = 200, description = "Success", body = BytedanceSeedream5LiteMultiFunctionImageGenResponse), + ), + params( + ("request" = BytedanceSeedream5LiteMultiFunctionImageGenRequest, description = "Payload for Request"), + ) +)] +pub async fn bytedance_seedream_5_lite_multi_function_image_gen_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data> +) -> Result, CommonWebError> { + + payments_error_test(&request.prompt.as_deref().unwrap_or(""))?; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await?; + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + let user_token = match maybe_user_session.as_ref() { + Some(session) => &session.user_token, + None => { + return Err(CommonWebError::NotAuthorized); + } + }; + + let image_urls = match request.image_media_tokens.as_ref() { + Some(media_tokens) => { + info!("Looking up image media tokens: {:?}", media_tokens); + lookup_image_urls_as_optional_list( + &http_request, + &mut mysql_connection, + server_state.server_environment, + media_tokens, + ).await? + }, + None => None, + }; + + insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string()) + })?; + + info!("Fal webhook URL: {}", server_state.fal.webhook_url); + + let apriori_job_token = InferenceJobToken::generate(); + + let fal_result; + + if let Some(input_image_urls) = image_urls.as_deref() { + info!("seedream 5 lite edit image"); + + let num_images = match request.num_images { + Some(BytedanceSeedream5LiteMultiFunctionImageGenNumImages::One) => EnqueueBytedanceSeedreamV5LiteEditImageNumImages::One, + Some(BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Two) => EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Two, + Some(BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Three) => EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Three, + Some(BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Four) => EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Four, + None => EnqueueBytedanceSeedreamV5LiteEditImageNumImages::One, + }; + + let image_size = match request.image_size { + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::Square) => EnqueueBytedanceSeedreamV5LiteEditImageSize::Square, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::SquareHd) => EnqueueBytedanceSeedreamV5LiteEditImageSize::SquareHd, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::PortraitFourThree) => EnqueueBytedanceSeedreamV5LiteEditImageSize::PortraitFourThree, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::PortraitSixteenNine) => EnqueueBytedanceSeedreamV5LiteEditImageSize::PortraitSixteenNine, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::LandscapeFourThree) => EnqueueBytedanceSeedreamV5LiteEditImageSize::LandscapeFourThree, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::LandscapeSixteenNine) => EnqueueBytedanceSeedreamV5LiteEditImageSize::LandscapeSixteenNine, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::Auto2k) => EnqueueBytedanceSeedreamV5LiteEditImageSize::Auto2k, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::Auto3k) => EnqueueBytedanceSeedreamV5LiteEditImageSize::Auto3k, + None => EnqueueBytedanceSeedreamV5LiteEditImageSize::SquareHd, + }; + + let args = EnqueueBytedanceSeedreamV5LiteEditImageArgs { + prompt: request.prompt.as_deref().unwrap_or(""), + image_urls: input_image_urls.to_owned(), + num_images: Some(num_images), + max_images: None, + image_size: Some(image_size), + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + }; + + let cost = args.calculate_cost_in_cents(); + + info!("Charging wallet: {}", cost); + + attempt_wallet_deduction_else_common_web_error( + user_token, + Some(apriori_job_token.as_str()), + cost, + &mut mysql_connection, + ).await?; + + fal_result = enqueue_bytedance_seedream_v5_lite_edit_image_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_bytedance_seedream_v5_lite_edit_image_webhook: {:?}", err); + CommonWebError::ServerError + })?; + + } else { + info!("seedream 5 lite text-to-image"); + + let num_images = match request.num_images { + Some(BytedanceSeedream5LiteMultiFunctionImageGenNumImages::One) => EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::One, + Some(BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Two) => EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Two, + Some(BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Three) => EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Three, + Some(BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Four) => EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Four, + None => EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::One, + }; + + let image_size = match request.image_size { + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::Square) => EnqueueBytedanceSeedreamV5LiteTextToImageSize::Square, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::SquareHd) => EnqueueBytedanceSeedreamV5LiteTextToImageSize::SquareHd, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::PortraitFourThree) => EnqueueBytedanceSeedreamV5LiteTextToImageSize::PortraitFourThree, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::PortraitSixteenNine) => EnqueueBytedanceSeedreamV5LiteTextToImageSize::PortraitSixteenNine, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::LandscapeFourThree) => EnqueueBytedanceSeedreamV5LiteTextToImageSize::LandscapeFourThree, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::LandscapeSixteenNine) => EnqueueBytedanceSeedreamV5LiteTextToImageSize::LandscapeSixteenNine, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::Auto2k) => EnqueueBytedanceSeedreamV5LiteTextToImageSize::Auto2k, + Some(BytedanceSeedream5LiteMultiFunctionImageGenImageSize::Auto3k) => EnqueueBytedanceSeedreamV5LiteTextToImageSize::Auto3k, + None => EnqueueBytedanceSeedreamV5LiteTextToImageSize::SquareHd, + }; + + let args = EnqueueBytedanceSeedreamV5LiteTextToImageArgs { + prompt: request.prompt.as_deref().unwrap_or(""), + num_images: Some(num_images), + max_images: None, + image_size: Some(image_size), + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + }; + + let cost = args.calculate_cost_in_cents(); + + info!("Charging wallet: {}", cost); + + attempt_wallet_deduction_else_common_web_error( + user_token, + Some(apriori_job_token.as_str()), + cost, + &mut mysql_connection, + ).await?; + + fal_result = enqueue_bytedance_seedream_v5_lite_text_to_image_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_bytedance_seedream_v5_lite_text_to_image_webhook: {:?}", err); + CommonWebError::ServerError + })?; + } + + let external_job_id = fal_result.request_id + .ok_or_else(|| { + warn!("Fal request_id is None"); + CommonWebError::ServerError + })?; + + info!("Fal request_id: {}", external_job_id); + + let ip_address = get_request_ip(&http_request); + + let mut transaction = mysql_connection + .begin() + .await + .map_err(|err| { + error!("Error starting MySQL transaction: {:?}", err); + CommonWebError::ServerError + })?; + + let prompt_result = insert_prompt(InsertPromptArgs { + maybe_apriori_prompt_token: None, + prompt_type: PromptType::ArtcraftApp, + maybe_creator_user_token: Some(&user_token), + maybe_model_type: Some(ModelType::Seedream5Lite), + maybe_generation_provider: Some(GenerationProvider::Artcraft), + maybe_positive_prompt: request.prompt.as_deref(), + maybe_negative_prompt: None, + maybe_other_args: None, + creator_ip_address: &ip_address, + mysql_executor: &mut *transaction, + phantom: Default::default(), + }).await; + + let prompt_token = match prompt_result { + Ok(token) => Some(token), + Err(err) => { + warn!("Error inserting prompt: {:?}", err); + None + } + }; + + if let Some(media_tokens) = &request.image_media_tokens { + if let Some(token) = prompt_token.as_ref() { + let result = insert_batch_prompt_context_items(InsertBatchArgs { + prompt_token: token.clone(), + items: media_tokens.iter().map(|token| { + PromptContextItem { + media_token: token.clone(), + context_semantic_type: PromptContextSemanticType::Imgref, + } + }).collect(), + transaction: &mut transaction, + }).await; + + if let Err(err) = result { + warn!("Error inserting batch prompt context items: {:?}", err); + } + } + } + + let db_result = insert_generic_inference_job_for_fal_queue_with_apriori_job_token(InsertGenericInferenceForFalWithAprioriJobTokenArgs { + apriori_job_token: &apriori_job_token, + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::ImageGeneration, + maybe_inference_args: None, + maybe_prompt_token: prompt_token.as_ref(), + maybe_creator_user_token: Some(&user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_executor: &mut *transaction, + starting_job_status_override: None, + phantom: Default::default(), + }).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting generic inference job for FAL queue: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + let _r = transaction + .commit() + .await + .map_err(|err| { + error!("Error committing MySQL transaction: {:?}", err); + CommonWebError::ServerError + })?; + + Ok(Json(BytedanceSeedream5LiteMultiFunctionImageGenResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/multi_function/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/multi_function/mod.rs index 237eafc4c..9f2edbed8 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/multi_function/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/multi_function/mod.rs @@ -1,5 +1,7 @@ pub mod bytedance_seedream_v4_multi_function_image_gen_handler; pub mod bytedance_seedream_v4p5_multi_function_image_gen_handler; +pub mod bytedance_seedream_5_lite_multi_function_image_gen_handler; pub mod gpt_image_1p5_multi_function_image_gen_handler; +pub mod nano_banana_2_multi_function_image_gen_handler; pub mod nano_banana_multi_function_image_gen_handler; pub mod nano_banana_pro_multi_function_image_gen_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/multi_function/nano_banana_2_multi_function_image_gen_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/multi_function/nano_banana_2_multi_function_image_gen_handler.rs new file mode 100644 index 000000000..71eef6290 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/multi_function/nano_banana_2_multi_function_image_gen_handler.rs @@ -0,0 +1,324 @@ +use std::sync::Arc; + +use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::state::server_state::ServerState; +use crate::util::lookup::lookup_image_urls_as_optional_list::lookup_image_urls_as_optional_list; +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; +use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::{NanaBanana2MultiFunctionImageGenAspectRatio, NanaBanana2MultiFunctionImageGenImageResolution, NanaBanana2MultiFunctionImageGenNumImages, NanaBanana2MultiFunctionImageGenRequest, NanaBanana2MultiFunctionImageGenResponse}; +use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType; +use enums::by_table::prompts::prompt_type::PromptType; +use enums::common::generation_provider::GenerationProvider; +use enums::common::model_type::ModelType; +use enums::common::visibility::Visibility; +use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator; +use fal_client::requests::webhook::image::edit::enqueue_nano_banana_2_edit_image_webhook::{enqueue_nano_banana_2_edit_image_webhook, EnqueueNanoBanana2EditImageArgs, EnqueueNanoBanana2EditImageAspectRatio, EnqueueNanoBanana2EditImageNumImages, EnqueueNanoBanana2EditImageResolution}; +use fal_client::requests::webhook::image::text::enqueue_nano_banana_2_text_to_image_webhook::{enqueue_nano_banana_2_text_to_image_webhook, EnqueueNanoBanana2TextToImageArgs, EnqueueNanoBanana2TextToImageAspectRatio, EnqueueNanoBanana2TextToImageNumImages, EnqueueNanoBanana2TextToImageResolution}; +use http_server_common::request::get_request_ip::get_request_ip; +use log::{error, info, warn}; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue_with_apriori_job_token::{insert_generic_inference_job_for_fal_queue_with_apriori_job_token, InsertGenericInferenceForFalWithAprioriJobTokenArgs}; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem}; +use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs}; +use sqlx::Acquire; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; + +/// Nano Banana 2 Multi-Function (generate + edit) +#[utoipa::path( + post, + tag = "Generate Images (Multi-Function)", + path = "/v1/generate/image/multi_function/nano_banana_2", + responses( + (status = 200, description = "Success", body = NanaBanana2MultiFunctionImageGenResponse), + ), + params( + ("request" = NanaBanana2MultiFunctionImageGenRequest, description = "Payload for Request"), + ) +)] +pub async fn nano_banana_2_multi_function_image_gen_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data> +) -> Result, CommonWebError> { + + payments_error_test(&request.prompt.as_deref().unwrap_or(""))?; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await?; + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + let user_token = match maybe_user_session.as_ref() { + Some(session) => &session.user_token, + None => { + return Err(CommonWebError::NotAuthorized); + } + }; + + let image_urls = match request.image_media_tokens.as_ref() { + Some(media_tokens) => { + info!("Looking up image media tokens: {:?}", media_tokens); + lookup_image_urls_as_optional_list( + &http_request, + &mut mysql_connection, + server_state.server_environment, + media_tokens, + ).await? + }, + None => None, + }; + + insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string()) + })?; + + info!("Fal webhook URL: {}", server_state.fal.webhook_url); + + let apriori_job_token = InferenceJobToken::generate(); + + let fal_result; + + if let Some(input_image_urls) = image_urls.as_deref() { + info!("nano banana 2 edit image"); + + let num_images = match request.num_images { + Some(NanaBanana2MultiFunctionImageGenNumImages::One) => EnqueueNanoBanana2EditImageNumImages::One, + Some(NanaBanana2MultiFunctionImageGenNumImages::Two) => EnqueueNanoBanana2EditImageNumImages::Two, + Some(NanaBanana2MultiFunctionImageGenNumImages::Three) => EnqueueNanoBanana2EditImageNumImages::Three, + Some(NanaBanana2MultiFunctionImageGenNumImages::Four) => EnqueueNanoBanana2EditImageNumImages::Four, + None => EnqueueNanoBanana2EditImageNumImages::One, + }; + + let resolution = match request.resolution { + Some(NanaBanana2MultiFunctionImageGenImageResolution::HalfK) => Some(EnqueueNanoBanana2EditImageResolution::HalfK), + Some(NanaBanana2MultiFunctionImageGenImageResolution::OneK) => Some(EnqueueNanoBanana2EditImageResolution::OneK), + Some(NanaBanana2MultiFunctionImageGenImageResolution::TwoK) => Some(EnqueueNanoBanana2EditImageResolution::TwoK), + Some(NanaBanana2MultiFunctionImageGenImageResolution::FourK) => Some(EnqueueNanoBanana2EditImageResolution::FourK), + None => Some(EnqueueNanoBanana2EditImageResolution::OneK), + }; + + let aspect_ratio = match request.aspect_ratio { + Some(NanaBanana2MultiFunctionImageGenAspectRatio::Auto) => Some(EnqueueNanoBanana2EditImageAspectRatio::Auto), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::OneByOne) => Some(EnqueueNanoBanana2EditImageAspectRatio::OneByOne), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::FiveByFour) => Some(EnqueueNanoBanana2EditImageAspectRatio::FiveByFour), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::FourByThree) => Some(EnqueueNanoBanana2EditImageAspectRatio::FourByThree), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::ThreeByTwo) => Some(EnqueueNanoBanana2EditImageAspectRatio::ThreeByTwo), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::SixteenByNine) => Some(EnqueueNanoBanana2EditImageAspectRatio::SixteenByNine), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::TwentyOneByNine) => Some(EnqueueNanoBanana2EditImageAspectRatio::TwentyOneByNine), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::FourByFive) => Some(EnqueueNanoBanana2EditImageAspectRatio::FourByFive), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::ThreeByFour) => Some(EnqueueNanoBanana2EditImageAspectRatio::ThreeByFour), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::TwoByThree) => Some(EnqueueNanoBanana2EditImageAspectRatio::TwoByThree), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::NineBySixteen) => Some(EnqueueNanoBanana2EditImageAspectRatio::NineBySixteen), + None => Some(EnqueueNanoBanana2EditImageAspectRatio::OneByOne), + }; + + let args = EnqueueNanoBanana2EditImageArgs { + prompt: request.prompt.as_deref().unwrap_or(""), + image_urls: input_image_urls.to_owned(), + num_images, + resolution, + aspect_ratio, + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + }; + + let cost = args.calculate_cost_in_cents(); + + info!("Charging wallet: {}", cost); + + attempt_wallet_deduction_else_common_web_error( + user_token, + Some(apriori_job_token.as_str()), + cost, + &mut mysql_connection, + ).await?; + + fal_result = enqueue_nano_banana_2_edit_image_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_nano_banana_2_edit_image_webhook: {:?}", err); + CommonWebError::ServerError + })?; + + } else { + info!("nano banana 2 text-to-image"); + + let num_images = match request.num_images { + Some(NanaBanana2MultiFunctionImageGenNumImages::One) => EnqueueNanoBanana2TextToImageNumImages::One, + Some(NanaBanana2MultiFunctionImageGenNumImages::Two) => EnqueueNanoBanana2TextToImageNumImages::Two, + Some(NanaBanana2MultiFunctionImageGenNumImages::Three) => EnqueueNanoBanana2TextToImageNumImages::Three, + Some(NanaBanana2MultiFunctionImageGenNumImages::Four) => EnqueueNanoBanana2TextToImageNumImages::Four, + None => EnqueueNanoBanana2TextToImageNumImages::One, + }; + + let resolution = match request.resolution { + Some(NanaBanana2MultiFunctionImageGenImageResolution::HalfK) => Some(EnqueueNanoBanana2TextToImageResolution::HalfK), + Some(NanaBanana2MultiFunctionImageGenImageResolution::OneK) => Some(EnqueueNanoBanana2TextToImageResolution::OneK), + Some(NanaBanana2MultiFunctionImageGenImageResolution::TwoK) => Some(EnqueueNanoBanana2TextToImageResolution::TwoK), + Some(NanaBanana2MultiFunctionImageGenImageResolution::FourK) => Some(EnqueueNanoBanana2TextToImageResolution::FourK), + None => Some(EnqueueNanoBanana2TextToImageResolution::OneK), + }; + + let aspect_ratio = match request.aspect_ratio { + // NB: "auto" is only for edit image, not text-to-image + Some(NanaBanana2MultiFunctionImageGenAspectRatio::Auto) => Some(EnqueueNanoBanana2TextToImageAspectRatio::OneByOne), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::OneByOne) => Some(EnqueueNanoBanana2TextToImageAspectRatio::OneByOne), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::FiveByFour) => Some(EnqueueNanoBanana2TextToImageAspectRatio::FiveByFour), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::FourByThree) => Some(EnqueueNanoBanana2TextToImageAspectRatio::FourByThree), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::ThreeByTwo) => Some(EnqueueNanoBanana2TextToImageAspectRatio::ThreeByTwo), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::SixteenByNine) => Some(EnqueueNanoBanana2TextToImageAspectRatio::SixteenByNine), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::TwentyOneByNine) => Some(EnqueueNanoBanana2TextToImageAspectRatio::TwentyOneByNine), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::FourByFive) => Some(EnqueueNanoBanana2TextToImageAspectRatio::FourByFive), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::ThreeByFour) => Some(EnqueueNanoBanana2TextToImageAspectRatio::ThreeByFour), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::TwoByThree) => Some(EnqueueNanoBanana2TextToImageAspectRatio::TwoByThree), + Some(NanaBanana2MultiFunctionImageGenAspectRatio::NineBySixteen) => Some(EnqueueNanoBanana2TextToImageAspectRatio::NineBySixteen), + None => Some(EnqueueNanoBanana2TextToImageAspectRatio::OneByOne), + }; + + let args = EnqueueNanoBanana2TextToImageArgs { + prompt: request.prompt.as_deref().unwrap_or(""), + num_images, + resolution, + aspect_ratio, + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + }; + + let cost = args.calculate_cost_in_cents(); + + info!("Charging wallet: {}", cost); + + attempt_wallet_deduction_else_common_web_error( + user_token, + Some(apriori_job_token.as_str()), + cost, + &mut mysql_connection, + ).await?; + + fal_result = enqueue_nano_banana_2_text_to_image_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_nano_banana_2_text_to_image_webhook: {:?}", err); + CommonWebError::ServerError + })?; + } + + let external_job_id = fal_result.request_id + .ok_or_else(|| { + warn!("Fal request_id is None"); + CommonWebError::ServerError + })?; + + info!("Fal request_id: {}", external_job_id); + + let ip_address = get_request_ip(&http_request); + + let mut transaction = mysql_connection + .begin() + .await + .map_err(|err| { + error!("Error starting MySQL transaction: {:?}", err); + CommonWebError::ServerError + })?; + + let prompt_result = insert_prompt(InsertPromptArgs { + maybe_apriori_prompt_token: None, + prompt_type: PromptType::ArtcraftApp, + maybe_creator_user_token: Some(&user_token), + maybe_model_type: Some(ModelType::NanoBanana2), + maybe_generation_provider: Some(GenerationProvider::Artcraft), + maybe_positive_prompt: request.prompt.as_deref(), + maybe_negative_prompt: None, + maybe_other_args: None, + creator_ip_address: &ip_address, + mysql_executor: &mut *transaction, + phantom: Default::default(), + }).await; + + let prompt_token = match prompt_result { + Ok(token) => Some(token), + Err(err) => { + warn!("Error inserting prompt: {:?}", err); + None + } + }; + + if let Some(media_tokens) = &request.image_media_tokens { + if let Some(token) = prompt_token.as_ref() { + let result = insert_batch_prompt_context_items(InsertBatchArgs { + prompt_token: token.clone(), + items: media_tokens.iter().map(|token| { + PromptContextItem { + media_token: token.clone(), + context_semantic_type: PromptContextSemanticType::Imgref, + } + }).collect(), + transaction: &mut transaction, + }).await; + + if let Err(err) = result { + warn!("Error inserting batch prompt context items: {:?}", err); + } + } + } + + let db_result = insert_generic_inference_job_for_fal_queue_with_apriori_job_token(InsertGenericInferenceForFalWithAprioriJobTokenArgs { + apriori_job_token: &apriori_job_token, + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::ImageGeneration, + maybe_inference_args: None, + maybe_prompt_token: prompt_token.as_ref(), + maybe_creator_user_token: Some(&user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_executor: &mut *transaction, + starting_job_status_override: None, + phantom: Default::default(), + }).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting generic inference job for FAL queue: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + let _r = transaction + .commit() + .await + .map_err(|err| { + error!("Error committing MySQL transaction: {:?}", err); + CommonWebError::ServerError + })?; + + Ok(Json(NanaBanana2MultiFunctionImageGenResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/generate_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/generate_routes.rs index 2eb4bcb6c..82a3a2eff 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/generate_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/generate_routes.rs @@ -14,7 +14,9 @@ use crate::http_server::endpoints::generate::image::inpaint::flux_dev_juggernaut use crate::http_server::endpoints::generate::image::inpaint::flux_pro_1_inpaint_handler::flux_pro_1_inpaint_image_handler; use crate::http_server::endpoints::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen_handler::bytedance_seedream_v4_multi_function_image_gen_handler; use crate::http_server::endpoints::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen_handler::bytedance_seedream_v4p5_multi_function_image_gen_handler; +use crate::http_server::endpoints::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen_handler::bytedance_seedream_5_lite_multi_function_image_gen_handler; use crate::http_server::endpoints::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen_handler::gpt_image_1p5_multi_function_image_gen_handler; +use crate::http_server::endpoints::generate::image::multi_function::nano_banana_2_multi_function_image_gen_handler::nano_banana_2_multi_function_image_gen_handler; use crate::http_server::endpoints::generate::image::multi_function::nano_banana_multi_function_image_gen_handler::nano_banana_multi_function_image_gen_handler; use crate::http_server::endpoints::generate::image::multi_function::nano_banana_pro_multi_function_image_gen_handler::nano_banana_pro_multi_function_image_gen_handler; use crate::http_server::endpoints::generate::image::remove_image_background_handler::remove_image_background_handler; @@ -81,10 +83,18 @@ where .route(web::post().to(nano_banana_multi_function_image_gen_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service(web::resource("/nano_banana_2") + .route(web::post().to(nano_banana_2_multi_function_image_gen_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) .service(web::resource("/nano_banana_pro") .route(web::post().to(nano_banana_pro_multi_function_image_gen_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service(web::resource("/bytedance_seedream_5_lite") + .route(web::post().to(bytedance_seedream_5_lite_multi_function_image_gen_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) ) .service(web::scope("/edit") .service(web::resource("/flux_pro_kontext_max")