Compare commits

...
Author SHA1 Message Date
Brandon Thomas f573dce150 Shouldn't be doing this yet 2026-03-04 09:18:04 -05:00
Brandon Thomas 4cc5425b38 fix names 2026-03-04 09:06:32 -05:00
Brandon Thomas dcac0fb352 make sure this runs 2026-03-04 09:00:09 -05:00
Brandon Thomas 1f29cb1f50 wip 2026-03-04 08:48:09 -05:00
Brandon Thomas 0a6b7933f4 plumb NBP and Seedream 5 throughout the stack 2026-03-04 08:07:30 -05:00
Brandon Thomas 79ccdc3ac3 new support for Nano Banana 2 and Seedream v5 Lite from Fal 2026-03-04 06:46:47 -05:00
46 changed files with 2651 additions and 12 deletions
@@ -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<String>,
/// 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<Vec<MediaFileToken>>,
/// Number of images to generate. Default is one.
pub num_images: Option<BytedanceSeedream5LiteMultiFunctionImageGenNumImages>,
pub max_images: Option<BytedanceSeedream5LiteMultiFunctionImageGenMaxImages>,
pub image_size: Option<BytedanceSeedream5LiteMultiFunctionImageGenImageSize>,
}
#[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,
}
@@ -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;
@@ -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<String>,
/// 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<Vec<MediaFileToken>>,
/// Number of images to generate. Default is one.
pub num_images: Option<NanaBanana2MultiFunctionImageGenNumImages>,
/// Resolution of the image to generate. Default is OneK (1K).
pub resolution: Option<NanaBanana2MultiFunctionImageGenImageResolution>,
/// Aspect ratio of the images to generate. Default is "1:1"
pub aspect_ratio: Option<NanaBanana2MultiFunctionImageGenAspectRatio>,
}
#[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,
}
@@ -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<BytedanceSeedream5LiteMultiFunctionImageGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
BYTEDANCE_SEEDREAM_5_LITE_MULTI_FUNCTION_IMAGE_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -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;
@@ -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<NanaBanana2MultiFunctionImageGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
NANO_BANANA_2_MULTI_FUNCTION_IMAGE_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -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,
}
@@ -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<CommonResolution>, 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);
}
}
@@ -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);
}
}
@@ -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;
@@ -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<GenerateImageResponse, ArtcraftRouterError> {
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.
}
}
@@ -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<GenerateImageResponse, ArtcraftRouterError> {
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.
}
}
@@ -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;
@@ -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),
@@ -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)
}
@@ -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;
@@ -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<MediaFileToken>>,
pub aspect_ratio: Option<NanaBanana2MultiFunctionImageGenAspectRatio>,
pub resolution: Option<NanaBanana2MultiFunctionImageGenImageResolution>,
pub num_images: NanaBanana2MultiFunctionImageGenNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_nano_banana_2<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftNanaBanana2<'a>, 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<ImageListRef<'a>>,
) -> Result<Option<&'a Vec<MediaFileToken>>, 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<CommonAspectRatio>,
is_edit_mode: bool,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<NanaBanana2MultiFunctionImageGenAspectRatio>, 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<CommonResolution>,
_strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<NanaBanana2MultiFunctionImageGenImageResolution>, 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<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<NanaBanana2MultiFunctionImageGenNumImages, ArtcraftRouterError> {
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,
);
}
}
}
@@ -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<MediaFileToken>>,
pub image_size: Option<BytedanceSeedream5LiteMultiFunctionImageGenImageSize>,
pub num_images: BytedanceSeedream5LiteMultiFunctionImageGenNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_seedream_5_lite<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftSeedream5Lite<'a>, 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<ImageListRef<'a>>,
) -> Result<Option<&'a Vec<MediaFileToken>>, 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<CommonAspectRatio>,
is_edit_mode: bool,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<BytedanceSeedream5LiteMultiFunctionImageGenImageSize>, 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<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<BytedanceSeedream5LiteMultiFunctionImageGenNumImages, ArtcraftRouterError> {
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,
);
}
}
}
@@ -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,
@@ -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<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<String>,
/// Eg. "0.5K", "1K", "2K", "4K"
/// Default: "1K"
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution: Option<String>,
/// 1 - 4
/// Default: 1
#[serde(skip_serializing_if = "Option::is_none")]
pub num_images: Option<u8>,
/// "jpeg", "png", "webp"
/// Default: "png"
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seed: Option<u64>,
/// 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<String>,
/// 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<bool>,
/// 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<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct NanoBanana2EditImageFile {
pub url: String,
//pub content_type: Option<String>,
//pub file_name: Option<String>,
//pub file_size: Option<u64>,
//pub width: Option<u32>,
//pub height: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct NanoBanana2EditImageOutput {
pub images: Vec<NanoBanana2EditImageFile>,
//pub description: String,
}
pub fn nano_banana_2_edit_image(
params: NanoBanana2EditImageInput,
) -> FalRequest<NanoBanana2EditImageInput, NanoBanana2EditImageOutput> {
FalRequest::new("fal-ai/nano-banana-2/edit", params)
}
@@ -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<String>,
/// Eg. "0.5K", "1K", "2K", "4K"
/// Default: "1K"
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution: Option<String>,
/// 1 - 4
/// Default: 1
#[serde(skip_serializing_if = "Option::is_none")]
pub num_images: Option<u8>,
/// 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<String>,
/// "jpeg", "png", "webp"
/// Default: "png"
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seed: Option<u64>,
/// 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<bool>,
/// 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<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct NanoBanana2ImageFile {
pub url: String,
//pub content_type: Option<String>,
//pub file_name: Option<String>,
//pub file_size: Option<u64>,
//pub width: Option<u32>,
//pub height: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct NanoBanana2TextToImageOutput {
pub images: Vec<NanoBanana2ImageFile>,
//pub description: String,
}
pub fn nano_banana_2_text_to_image(
params: NanoBanana2TextToImageInput,
) -> FalRequest<NanoBanana2TextToImageInput, NanoBanana2TextToImageOutput> {
FalRequest::new("fal-ai/nano-banana-2", params)
}
@@ -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<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<String>,
/// Default: 1
#[serde(skip_serializing_if = "Option::is_none")]
pub num_images: Option<u8>,
/// Default: 1
#[serde(skip_serializing_if = "Option::is_none")]
pub max_images: Option<u8>,
/// Default: true
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_safety_checker: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SeedreamV5LiteEditImageFile {
pub url: String,
//pub content_type: Option<String>,
//pub file_name: Option<String>,
//pub file_size: Option<u64>,
//pub width: Option<u32>,
//pub height: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SeedreamV5LiteEditImageOutput {
pub images: Vec<SeedreamV5LiteEditImageFile>,
pub seed: u64,
}
pub fn http_seedream_5_edit_image(
params: SeedreamV5LiteEditImageInput,
) -> FalRequest<SeedreamV5LiteEditImageInput, SeedreamV5LiteEditImageOutput> {
FalRequest::new("fal-ai/bytedance/seedream/v5/lite/edit", params)
}
@@ -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<String>,
/// Default: 1
#[serde(skip_serializing_if = "Option::is_none")]
pub num_images: Option<u8>,
/// Default: 1
#[serde(skip_serializing_if = "Option::is_none")]
pub max_images: Option<u8>,
/// Default: true
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_safety_checker: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SeedreamV5LiteImageFile {
pub url: String,
//pub content_type: Option<String>,
//pub file_name: Option<String>,
//pub file_size: Option<u64>,
//pub width: Option<u32>,
//pub height: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SeedreamV5LiteTextToImageOutput {
pub images: Vec<SeedreamV5LiteImageFile>,
pub seed: u64,
}
pub fn http_seedream_5_text_to_image(
params: SeedreamV5LiteTextToImageInput,
) -> FalRequest<SeedreamV5LiteTextToImageInput, SeedreamV5LiteTextToImageOutput> {
FalRequest::new("fal-ai/bytedance/seedream/v5/lite/text-to-image", params)
}
@@ -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;
@@ -0,0 +1 @@
pub mod image;
@@ -1,3 +1,4 @@
pub mod http;
pub mod queue;
pub mod webhook;
pub mod traits;
pub mod traits;
pub mod webhook;
@@ -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<String>,
// Optional args
pub num_images: Option<EnqueueBytedanceSeedreamV5LiteEditImageNumImages>,
pub max_images: Option<EnqueueBytedanceSeedreamV5LiteEditImageMaxImages>,
pub image_size: Option<EnqueueBytedanceSeedreamV5LiteEditImageSize>,
// 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 <U: IntoUrl> 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<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV5LiteEditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
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(())
}
}
@@ -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<String>,
pub num_images: EnqueueNanoBanana2EditImageNumImages,
// Optional args
pub resolution: Option<EnqueueNanoBanana2EditImageResolution>,
pub aspect_ratio: Option<EnqueueNanoBanana2EditImageAspectRatio>,
// 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 <U: IntoUrl> 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<R: IntoUrl>(
args: EnqueueNanoBanana2EditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
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(())
}
}
@@ -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;
@@ -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<EnqueueBytedanceSeedreamV5LiteTextToImageNumImages>,
pub max_images: Option<EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages>,
pub image_size: Option<EnqueueBytedanceSeedreamV5LiteTextToImageSize>,
// 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 <U: IntoUrl> 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<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV5LiteTextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
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(())
}
}
@@ -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<EnqueueNanoBanana2TextToImageResolution>,
pub aspect_ratio: Option<EnqueueNanoBanana2TextToImageAspectRatio>,
// 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 <U: IntoUrl> 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<R: IntoUrl>(
args: EnqueueNanoBanana2TextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
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(())
}
}
@@ -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;
@@ -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,
}
}
@@ -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,
}
@@ -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,
}
}
@@ -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,
}
}
@@ -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")]
@@ -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,
}
}
@@ -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,
@@ -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,
}
@@ -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));
@@ -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, HandlerE
CommonImageModel::FluxPro11Ultra => 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)
}
@@ -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<BytedanceSeedream5LiteMultiFunctionImageGenRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<BytedanceSeedream5LiteMultiFunctionImageGenResponse>, 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,
}))
}
@@ -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;
@@ -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<NanaBanana2MultiFunctionImageGenRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<NanaBanana2MultiFunctionImageGenResponse>, 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,
}))
}
@@ -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")