Compare commits

...
Author SHA1 Message Date
Brandon Thomas 442bc9dc02 tests 2026-03-03 19:03:27 -05:00
Brandon Thomas 72058f7427 prompts 2026-03-03 18:45:17 -05:00
Brandon Thomas a7e22292f5 move seedream 4/4.5 to artcraft_router 2026-03-03 18:42:30 -05:00
25 changed files with 1329 additions and 530 deletions
Generated
+34 -12
View File
@@ -791,6 +791,7 @@ dependencies = [
"seedance2pro",
"serde",
"serde_derive",
"strum 0.28.0",
"test_data",
"tokens",
"tokio",
@@ -1523,7 +1524,7 @@ dependencies = [
"bitflags 2.11.0",
"cexpr",
"clang-sys",
"itertools 0.11.0",
"itertools 0.13.0",
"proc-macro2",
"quote",
"regex",
@@ -3471,7 +3472,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.7.4",
"libloading 0.8.6",
]
[[package]]
@@ -3776,7 +3777,7 @@ dependencies = [
"serde_json",
"sqlx",
"storyteller_root",
"strum",
"strum 0.26.2",
"tokens",
"tokio",
"workspace-build-acceleration",
@@ -3938,7 +3939,7 @@ dependencies = [
"sqlx",
"sqlx-core",
"sqlx-mysql",
"strum",
"strum 0.26.2",
"toml 0.8.23",
"utoipa",
"workspace-build-acceleration",
@@ -3952,7 +3953,7 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"strum",
"strum 0.26.2",
"toml 0.8.23",
"utoipa",
"workspace-build-acceleration",
@@ -6501,7 +6502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -7066,7 +7067,7 @@ dependencies = [
"serde_json",
"sqlx",
"storyteller_root",
"strum",
"strum 0.26.2",
"tempdir",
"tokens",
"tokio",
@@ -11581,8 +11582,8 @@ dependencies = [
"server_environment",
"speculoos",
"sqlx",
"strum",
"strum_macros",
"strum 0.26.2",
"strum_macros 0.26.4",
"tempdir",
"tempfile",
"testing",
@@ -11667,7 +11668,16 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
dependencies = [
"strum_macros",
"strum_macros 0.26.4",
]
[[package]]
name = "strum"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
dependencies = [
"strum_macros 0.28.0",
]
[[package]]
@@ -11683,6 +11693,18 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "strum_macros"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "subprocess"
version = "0.2.9"
@@ -12679,7 +12701,7 @@ dependencies = [
"sqlx",
"sqlx-core",
"sqlx-mysql",
"strum",
"strum 0.26.2",
"utoipa",
"workspace-build-acceleration",
]
@@ -14029,7 +14051,7 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"strum",
"strum 0.26.2",
"workspace-build-acceleration",
]
+2 -1
View File
@@ -283,12 +283,13 @@ redis = { version = "1.0.2", features = ["r2d2", "tokio-rustls-comp", "tokio-com
regex = "1.12.3"
reqwest = { version = "0.12.23", default-features = false, features = [ "charset", "http2", "system-proxy", "json", "cookies", "rustls-tls", "gzip" ] }
reqwest-websocket = { version = "0.5.0" }
serde = { version = "1.0.219", features = ["derive"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_derive = "1.0.219"
serde_json = "1.0.140"
serde_yaml = "0.9.34+deprecated"
sha2 = "0.11.0-rc.2"
sha3 = "0.10.8"
strum = { version = "0.28.0", features = ["derive"] }
tauri = { version = "2.8.3", features = ["unstable", "devtools"] } # NB: "devtools" prevents App Store distribution; "ubstable" is needed for window access
tauri-plugin-dialog = "2.3.3"
tauri-plugin-fs = "2.4.2"
@@ -32,3 +32,5 @@ uuid.workspace = true
[dev-dependencies]
test_data.workspace = true
strum.workspace = true
@@ -1,9 +1,12 @@
use serde_derive::{Deserialize, Serialize};
#[cfg(test)]
use strum::EnumIter;
/// This is a comprehensive list of common aspect ratios you can specify when enqueuing a generation.
/// Not every model will support every aspect ratio.
/// In the case a model doesn't support the aspect ratio, gracefully pick the nearest option.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(EnumIter))]
#[serde(rename_all = "snake_case")]
pub enum CommonAspectRatio {
// Auto (eg. for image editing to use the source; used by Nano Banana Pro edit image, but not text-to-image)
@@ -7,4 +7,6 @@ use serde_derive::{Deserialize, Serialize};
pub enum CommonImageModel {
#[serde(rename = "nano_banana_pro")]
NanaBananaPro,
Seedream4,
Seedream4p5,
}
@@ -0,0 +1,65 @@
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::BytedanceSeedreamV4MultiFunctionImageGenNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::PlanArtcraftSeedream4;
pub(crate) fn estimate_image_cost_artcraft_seedream_4(
plan: &PlanArtcraftSeedream4<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: $0.03/image (3 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 3;
let num_images: u64 = match plan.num_images {
BytedanceSeedreamV4MultiFunctionImageGenNumImages::One => 1,
BytedanceSeedreamV4MultiFunctionImageGenNumImages::Two => 2,
BytedanceSeedreamV4MultiFunctionImageGenNumImages::Three => 3,
BytedanceSeedreamV4MultiFunctionImageGenNumImages::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::Seedream4,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.03/image = 3 cents each
assert_eq!(estimate_usd_cents(1), 3);
assert_eq!(estimate_usd_cents(2), 6);
assert_eq!(estimate_usd_cents(3), 9);
assert_eq!(estimate_usd_cents(4), 12);
}
}
@@ -0,0 +1,65 @@
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::BytedanceSeedreamV4p5MultiFunctionImageGenNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::PlanArtcraftSeedream4p5;
pub(crate) fn estimate_image_cost_artcraft_seedream_4p5(
plan: &PlanArtcraftSeedream4p5<'_>,
) -> 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 {
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::One => 1,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Two => 2,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Three => 3,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::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::Seedream4p5,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.04/image = 4 cents each
assert_eq!(estimate_usd_cents(1), 4);
assert_eq!(estimate_usd_cents(2), 8);
assert_eq!(estimate_usd_cents(3), 12);
assert_eq!(estimate_usd_cents(4), 16);
}
}
@@ -1 +1,3 @@
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;
@@ -0,0 +1,160 @@
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_4::PlanArtcraftSeedream4;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::BytedanceSeedreamV4MultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen_image::bytedance_seedream_v4_multi_function_image_gen;
pub async fn execute_artcraft_seedream_4(
plan: &PlanArtcraftSeedream4<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = BytedanceSeedreamV4MultiFunctionImageGenRequest {
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_v4_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_seedream_4_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_seedream_4() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_seedream_4_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_seedream_4() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequest {
prompt: Some("Change the background to a sunny beach"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(CommonAspectRatio::Auto),
image_batch_count: Some(1),
..base_seedream_4_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_seedream_4_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
image_batch_count: Some(1),
prompt: Some("a shiba walking through a cyberpunk city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_edit_image_seedream_4_all_aspect_ratios() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
prompt: Some("Change the background to a sunny beach"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(ar),
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -0,0 +1,160 @@
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_4p5::PlanArtcraftSeedream4p5;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::BytedanceSeedreamV4p5MultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen_image::bytedance_seedream_v4p5_multi_function_image_gen;
pub async fn execute_artcraft_seedream_4p5(
plan: &PlanArtcraftSeedream4p5<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = BytedanceSeedreamV4p5MultiFunctionImageGenRequest {
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_v4p5_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_seedream_4p5_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_seedream_4p5() {
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_4p5_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_seedream_4p5() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequest {
prompt: Some("Change the background to a spooky haunted house"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(CommonAspectRatio::Auto2k),
image_batch_count: Some(1),
..base_seedream_4p5_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_seedream_4p5_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
image_batch_count: Some(1),
prompt: Some("a corgi walking through a cyberpunk city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4p5_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_edit_image_seedream_4p5_all_aspect_ratios() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
prompt: Some("Change the background to a spooky haunted house"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(ar),
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4p5_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1 +1,3 @@
pub mod generate_image_artcraft_nano_banana_pro;
pub mod generate_image_artcraft_seedream_4;
pub mod generate_image_artcraft_seedream_4p5;
@@ -5,8 +5,11 @@ use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
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::image_generation_plan::ImageGenerationPlan;
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::fal::plan_generate_image_fal_nano_banana_pro::plan_generate_image_fal_nano_banana_pro;
pub struct GenerateImageRequest<'a> {
@@ -49,11 +52,23 @@ impl<'a> GenerateImageRequest<'a> {
CommonImageModel::NanaBananaPro => {
plan_generate_image_artcraft_nano_banana_pro(self).map(ImageGenerationPlan::ArtcraftNanaBananaPro)
}
CommonImageModel::Seedream4 => {
plan_generate_image_artcraft_seedream_4(self).map(ImageGenerationPlan::ArtcraftSeedream4)
}
CommonImageModel::Seedream4p5 => {
plan_generate_image_artcraft_seedream_4p5(self).map(ImageGenerationPlan::ArtcraftSeedream4p5)
}
},
Provider::Fal => match self.model {
CommonImageModel::NanaBananaPro => {
plan_generate_image_fal_nano_banana_pro(self).map(ImageGenerationPlan::FalNanaBananaPro)
}
CommonImageModel::Seedream4 | CommonImageModel::Seedream4p5 => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "provider",
value: "Seedream is only available on the Artcraft provider".to_string(),
}))
}
},
}
}
@@ -1,17 +1,25 @@
use crate::client::router_client::RouterClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_nano_banana_pro::estimate_image_cost_artcraft_nano_banana_pro;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4::estimate_image_cost_artcraft_seedream_4;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4p5::estimate_image_cost_artcraft_seedream_4p5;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_pro::estimate_image_cost_fal_nano_banana_pro;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana_pro::execute_artcraft_nano_banana_pro;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4::execute_artcraft_seedream_4;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4p5::execute_artcraft_seedream_4p5;
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_pro::execute_fal_nano_banana_pro;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_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::fal::plan_generate_image_fal_nano_banana_pro::PlanFalNanaBananaPro;
#[derive(Debug)]
pub enum ImageGenerationPlan<'a> {
ArtcraftNanaBananaPro(PlanArtcraftNanaBananaPro<'a>),
ArtcraftSeedream4(PlanArtcraftSeedream4<'a>),
ArtcraftSeedream4p5(PlanArtcraftSeedream4p5<'a>),
FalNanaBananaPro(PlanFalNanaBananaPro<'a>),
}
@@ -25,6 +33,14 @@ impl<'a> ImageGenerationPlan<'a> {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_nano_banana_pro(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftSeedream4(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_seedream_4(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftSeedream4p5(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_seedream_4p5(plan, artcraft_client).await
}
ImageGenerationPlan::FalNanaBananaPro(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_nano_banana_pro(plan, fal_client).await
@@ -37,6 +53,12 @@ impl<'a> ImageGenerationPlan<'a> {
ImageGenerationPlan::ArtcraftNanaBananaPro(plan) => {
estimate_image_cost_artcraft_nano_banana_pro(plan)
}
ImageGenerationPlan::ArtcraftSeedream4(plan) => {
estimate_image_cost_artcraft_seedream_4(plan)
}
ImageGenerationPlan::ArtcraftSeedream4p5(plan) => {
estimate_image_cost_artcraft_seedream_4p5(plan)
}
ImageGenerationPlan::FalNanaBananaPro(plan) => {
estimate_image_cost_fal_nano_banana_pro(plan)
}
@@ -1 +1,3 @@
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;
@@ -0,0 +1,380 @@
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_v4_multi_function_image_gen::{
BytedanceSeedreamV4MultiFunctionImageGenImageSize,
BytedanceSeedreamV4MultiFunctionImageGenNumImages,
};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftSeedream4<'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<BytedanceSeedreamV4MultiFunctionImageGenImageSize>,
pub num_images: BytedanceSeedreamV4MultiFunctionImageGenNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_seedream_4<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftSeedream4<'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(PlanArtcraftSeedream4 {
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 V4 supported image sizes:
// Square, SquareHd
// PortraitFourThree, PortraitSixteenNine
// LandscapeFourThree, LandscapeSixteenNine
// Auto (edit mode: preserve source dimensions), Auto2k, Auto4k
fn plan_image_size(
aspect_ratio: Option<CommonAspectRatio>,
is_edit_mode: bool,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<BytedanceSeedreamV4MultiFunctionImageGenImageSize>, ArtcraftRouterError> {
use BytedanceSeedreamV4MultiFunctionImageGenImageSize as S;
match aspect_ratio {
None => Ok(None),
// Auto: valid in edit mode (preserve source dimensions); fall back to Square for text-to-image
Some(CommonAspectRatio::Auto) => {
if is_edit_mode { Ok(Some(S::Auto)) } else { Ok(Some(S::Square)) }
}
Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
// 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<BytedanceSeedreamV4MultiFunctionImageGenNumImages, ArtcraftRouterError> {
use BytedanceSeedreamV4MultiFunctionImageGenNumImages 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_4_image_request;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::{
BytedanceSeedreamV4MultiFunctionImageGenImageSize as S,
BytedanceSeedreamV4MultiFunctionImageGenNumImages as N,
};
// ── Image size (from aspect ratio) ──────────────────────────────────────────
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(plan.image_size.is_none());
}
#[test]
fn image_size_square() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
let expected = match ar {
CommonAspectRatio::Square => S::Square,
CommonAspectRatio::SquareHd => S::SquareHd,
_ => unreachable!(),
};
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_wide_direct() {
let cases = [
(CommonAspectRatio::Wide, S::LandscapeSixteenNine),
(CommonAspectRatio::WideSixteenByNine, S::LandscapeSixteenNine),
(CommonAspectRatio::WideFourByThree, S::LandscapeFourThree),
];
for (ar, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_tall_direct() {
let cases = [
(CommonAspectRatio::Tall, S::PortraitSixteenNine),
(CommonAspectRatio::TallNineBySixteen, S::PortraitSixteenNine),
(CommonAspectRatio::TallThreeByFour, S::PortraitFourThree),
];
for (ar, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_auto_in_edit_mode_yields_auto() {
let tokens = vec![];
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_seedream_4_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(matches!(plan.image_size, Some(S::Auto)));
}
#[test]
fn image_size_auto_in_text_to_image_yields_square() {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: None,
..base_seedream_4_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(matches!(plan.image_size, Some(S::Square)));
}
#[test]
fn image_size_auto2k_and_auto4k() {
for (ar, expected) in [(CommonAspectRatio::Auto2k, S::Auto2k), (CommonAspectRatio::Auto4k, S::Auto4k)] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_unsupported_error_out() {
for ar in [CommonAspectRatio::WideFiveByFour, CommonAspectRatio::TallFourByFive] {
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_seedream_4_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
), "expected error for {:?}", ar);
}
}
#[test]
fn image_size_unsupported_nearest_match() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideFiveByFour),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(matches!(plan.image_size, Some(S::LandscapeSixteenNine)), "expected LandscapeSixteenNine with {:?}", strategy);
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(matches!(plan.image_size, Some(S::PortraitSixteenNine)), "expected PortraitSixteenNine with {:?}", strategy);
}
}
// ── Num images ──────────────────────────────────────────────────────────────
#[test]
fn num_images_zero_is_always_error() {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4_image_request()
};
assert!(
matches!(request.build(), Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
"expected UserRequestedZeroGenerations with {:?}", strategy,
);
}
}
#[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_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
"expected {:?} for count {}", expected, count,
);
}
}
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_seedream_4_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(matches!(plan.num_images, N::Four), "expected Four for count 5 with {:?}", strategy);
}
}
// ── Image inputs ──────────────────────────────────────────────────────────
#[test]
fn url_image_inputs_returns_error() {
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
..base_seedream_4_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
));
}
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(plan.image_inputs.is_none());
}
#[test]
fn media_token_image_inputs_is_edit_mode() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_seedream_4_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(plan.image_inputs.is_some());
}
}
@@ -0,0 +1,381 @@
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_v4p5_multi_function_image_gen::{
BytedanceSeedreamV4p5MultiFunctionImageGenImageSize,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages,
};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftSeedream4p5<'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<BytedanceSeedreamV4p5MultiFunctionImageGenImageSize>,
pub num_images: BytedanceSeedreamV4p5MultiFunctionImageGenNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_seedream_4p5<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftSeedream4p5<'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(PlanArtcraftSeedream4p5 {
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 V4.5 supported image sizes:
// Square, SquareHd
// PortraitFourThree, PortraitSixteenNine
// LandscapeFourThree, LandscapeSixteenNine
// Auto2k, Auto4k (no bare Auto — unlike V4)
fn plan_image_size(
aspect_ratio: Option<CommonAspectRatio>,
is_edit_mode: bool,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<BytedanceSeedreamV4p5MultiFunctionImageGenImageSize>, ArtcraftRouterError> {
use BytedanceSeedreamV4p5MultiFunctionImageGenImageSize as S;
match aspect_ratio {
None => Ok(None),
// Auto: V4.5 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)),
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
// 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<BytedanceSeedreamV4p5MultiFunctionImageGenNumImages, ArtcraftRouterError> {
use BytedanceSeedreamV4p5MultiFunctionImageGenNumImages 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_4p5_image_request;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::{
BytedanceSeedreamV4p5MultiFunctionImageGenImageSize as S,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages as N,
};
// ── Image size (from aspect ratio) ──────────────────────────────────────────
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(plan.image_size.is_none());
}
#[test]
fn image_size_square() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
let expected = match ar {
CommonAspectRatio::Square => S::Square,
CommonAspectRatio::SquareHd => S::SquareHd,
_ => unreachable!(),
};
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_wide_direct() {
let cases = [
(CommonAspectRatio::Wide, S::LandscapeSixteenNine),
(CommonAspectRatio::WideSixteenByNine, S::LandscapeSixteenNine),
(CommonAspectRatio::WideFourByThree, S::LandscapeFourThree),
];
for (ar, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_tall_direct() {
let cases = [
(CommonAspectRatio::Tall, S::PortraitSixteenNine),
(CommonAspectRatio::TallNineBySixteen, S::PortraitSixteenNine),
(CommonAspectRatio::TallThreeByFour, S::PortraitFourThree),
];
for (ar, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_auto_in_edit_mode_yields_auto2k() {
// V4.5 has no bare Auto; falls back to Auto2k in edit mode
let tokens = vec![];
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_seedream_4p5_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
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_4p5_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(matches!(plan.image_size, Some(S::Square)));
}
#[test]
fn image_size_auto2k_and_auto4k() {
for (ar, expected) in [(CommonAspectRatio::Auto2k, S::Auto2k), (CommonAspectRatio::Auto4k, S::Auto4k)] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_unsupported_error_out() {
for ar in [CommonAspectRatio::WideFiveByFour, CommonAspectRatio::TallFourByFive] {
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_seedream_4p5_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
), "expected error for {:?}", ar);
}
}
#[test]
fn image_size_unsupported_nearest_match() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideFiveByFour),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4p5_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(matches!(plan.image_size, Some(S::LandscapeSixteenNine)), "expected LandscapeSixteenNine with {:?}", strategy);
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4p5_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(matches!(plan.image_size, Some(S::PortraitSixteenNine)), "expected PortraitSixteenNine with {:?}", strategy);
}
}
// ── Num images ──────────────────────────────────────────────────────────────
#[test]
fn num_images_zero_is_always_error() {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4p5_image_request()
};
assert!(
matches!(request.build(), Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
"expected UserRequestedZeroGenerations with {:?}", strategy,
);
}
}
#[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_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
"expected {:?} for count {}", expected, count,
);
}
}
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_seedream_4p5_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4p5_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(matches!(plan.num_images, N::Four), "expected Four for count 5 with {:?}", strategy);
}
}
// ── Image inputs ──────────────────────────────────────────────────────────
#[test]
fn url_image_inputs_returns_error() {
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
..base_seedream_4p5_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
));
}
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(plan.image_inputs.is_none());
}
#[test]
fn media_token_image_inputs_is_edit_mode() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_seedream_4p5_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(plan.image_inputs.is_some());
}
}
@@ -57,6 +57,34 @@ pub fn base_fal_image_request() -> GenerateImageRequest<'static> {
}
}
pub fn base_seedream_4_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::Seedream4,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
}
}
pub fn base_seedream_4p5_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::Seedream4p5,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
}
}
pub fn base_video_request() -> GenerateVideoRequest<'static> {
GenerateVideoRequest {
model: CommonVideoModel::Seedance2p0,
@@ -1,137 +0,0 @@
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_nano_banana_pro::aspect_ratio_to_artcraft_nano_banana_pro;
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_seedream_4::aspect_ratio_to_artcraft_seedream_4;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::{BytedanceSeedreamV4MultiFunctionImageGenImageSize, BytedanceSeedreamV4MultiFunctionImageGenNumImages, BytedanceSeedreamV4MultiFunctionImageGenRequest};
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::NanoBananaProMultiFunctionImageGenAspectRatio;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen_image::bytedance_seedream_v4_multi_function_image_gen;
use tauri::AppHandle;
const MAX_IMAGES: usize = 4;
pub async fn handle_artcraft_seedream_4_edit(
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(GenerateError::needs_storyteller_credentials());
},
};
info!("Calling Artcraft seedream 4 (edit) ...");
let uuid_idempotency_token = generate_random_uuid();
let image_size = get_aspect_ratio(request);
let num_images = match request.image_count {
None => None,
Some(1) => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::One),
Some(2) => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Two),
Some(3) => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Three),
Some(4) => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Four),
Some(other) => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
}));
},
};
let mut media_tokens = Vec::with_capacity(10);
if let Some(scene_image_media_token) = request.scene_image_media_token.clone() {
media_tokens.push(scene_image_media_token);
}
if let Some(image_media_tokens) = request.image_media_tokens.as_ref() {
media_tokens.extend_from_slice(image_media_tokens);
}
if media_tokens.len() > MAX_IMAGES {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages {
min: 1,
max: MAX_IMAGES as u32,
provided: media_tokens.len() as u32,
}));
}
let request = BytedanceSeedreamV4MultiFunctionImageGenRequest {
uuid_idempotency_token,
prompt: Some(request.prompt.clone()),
image_media_tokens: Some(media_tokens),
image_size,
num_images,
// Not provided
max_images: None,
};
let result = bytedance_seedream_v4_multi_function_image_gen(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft seedream 4. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft seedream 4: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::Seedream4),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
})
}
fn get_aspect_ratio(request: &EnqueueEditImageCommand) -> Option<BytedanceSeedreamV4MultiFunctionImageGenImageSize> {
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
// Handle modern aspect ratio
let aspect = aspect_ratio_to_artcraft_seedream_4(common_aspect_ratio);
return Some(aspect);
}
if let Some(aspect_ratio) = request.aspect_ratio {
// Handle deprecated aspect ratio
let aspect_ratio = match aspect_ratio {
EditImageSize::Auto => BytedanceSeedreamV4MultiFunctionImageGenImageSize::Square,
EditImageSize::Square => BytedanceSeedreamV4MultiFunctionImageGenImageSize::Square,
EditImageSize::Tall => BytedanceSeedreamV4MultiFunctionImageGenImageSize::PortraitSixteenNine,
EditImageSize::Wide => BytedanceSeedreamV4MultiFunctionImageGenImageSize::LandscapeSixteenNine,
};
return Some(aspect_ratio);
}
None
}
@@ -1,136 +0,0 @@
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_seedream_4::aspect_ratio_to_artcraft_seedream_4;
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_seedream_4p5::aspect_ratio_to_artcraft_seedream_4p5;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::{BytedanceSeedreamV4p5MultiFunctionImageGenImageSize, BytedanceSeedreamV4p5MultiFunctionImageGenNumImages, BytedanceSeedreamV4p5MultiFunctionImageGenRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen_image::bytedance_seedream_v4p5_multi_function_image_gen;
use tauri::AppHandle;
const MAX_IMAGES: usize = 4;
pub async fn handle_artcraft_seedream_4p5_edit(
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(GenerateError::needs_storyteller_credentials());
},
};
info!("Calling Artcraft seedream 4.5 (edit) ...");
let uuid_idempotency_token = generate_random_uuid();
let image_size = get_aspect_ratio(request);
let num_images = match request.image_count {
None => None,
Some(1) => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::One),
Some(2) => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Two),
Some(3) => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Three),
Some(4) => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Four),
Some(other) => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
}));
},
};
let mut media_tokens = Vec::with_capacity(10);
if let Some(scene_image_media_token) = request.scene_image_media_token.clone() {
media_tokens.push(scene_image_media_token);
}
if let Some(image_media_tokens) = request.image_media_tokens.as_ref() {
media_tokens.extend_from_slice(image_media_tokens);
}
if media_tokens.len() > MAX_IMAGES {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages {
min: 1,
max: MAX_IMAGES as u32,
provided: media_tokens.len() as u32,
}));
}
let request = BytedanceSeedreamV4p5MultiFunctionImageGenRequest {
uuid_idempotency_token,
prompt: Some(request.prompt.clone()),
image_media_tokens: Some(media_tokens),
image_size,
num_images,
// Not provided
max_images: None,
};
let result = bytedance_seedream_v4p5_multi_function_image_gen(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft seedream 4.5. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft seedream 4.5: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::Seedream4p5),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
})
}
fn get_aspect_ratio(request: &EnqueueEditImageCommand) -> Option<BytedanceSeedreamV4p5MultiFunctionImageGenImageSize> {
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
// Handle modern aspect ratio
let aspect = aspect_ratio_to_artcraft_seedream_4p5(common_aspect_ratio);
return Some(aspect);
}
if let Some(aspect_ratio) = request.aspect_ratio {
// Handle deprecated aspect ratio
let aspect_ratio = match aspect_ratio {
EditImageSize::Auto => BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::Square,
EditImageSize::Square => BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::Square,
EditImageSize::Tall => BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::PortraitSixteenNine,
EditImageSize::Wide => BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::LandscapeSixteenNine,
};
return Some(aspect_ratio);
}
None
}
@@ -4,8 +4,6 @@ use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_gpt_im
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_gpt_image_1p5_edit::handle_artcraft_gpt_image_1p5_edit;
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_nano_banana_edit::handle_artcraft_nano_banana_edit;
use crate::core::commands::enqueue::image_edit::artcraft::handle_image_edit_artcraft_via_router::handle_image_edit_artcraft_via_router;
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_seedream_4_edit::handle_artcraft_seedream_4_edit;
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_seedream_4p5_edit::handle_artcraft_seedream_4p5_edit;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EnqueueEditImageCommand, ImageEditModel};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::generation_events::common::GenerationModel;
@@ -32,7 +30,7 @@ pub async fn handle_image_edit_artcraft(
ImageEditModel::NanoBananaPro => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBananaPro, GenerationModel::NanoBananaPro).await,
ImageEditModel::GptImage1 => handle_artcraft_gpt_image_1_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await,
ImageEditModel::GptImage1p5 => handle_artcraft_gpt_image_1p5_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await,
ImageEditModel::Seedream4 => handle_artcraft_seedream_4_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await,
ImageEditModel::Seedream4p5 => handle_artcraft_seedream_4p5_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).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,
}
}
@@ -3,7 +3,5 @@ mod handle_artcraft_flux_kontext_edit;
mod handle_artcraft_gpt_image_1_edit;
mod handle_artcraft_gpt_image_1p5_edit;
mod handle_artcraft_nano_banana_edit;
mod handle_artcraft_seedream_4_edit;
mod handle_artcraft_seedream_4p5_edit;
pub (super) mod handle_image_edit_artcraft;
@@ -1,116 +0,0 @@
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_seedream_4::aspect_ratio_to_artcraft_seedream_4;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::{BytedanceSeedreamV4MultiFunctionImageGenImageSize, BytedanceSeedreamV4MultiFunctionImageGenNumImages, BytedanceSeedreamV4MultiFunctionImageGenRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use std::time::Duration;
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen_image::bytedance_seedream_v4_multi_function_image_gen;
use tauri::AppHandle;
pub async fn handle_artcraft_seedream_4_text_to_image(
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(GenerateError::needs_storyteller_credentials());
},
};
info!("Calling Artcraft seedream 4 ...");
let uuid_idempotency_token = generate_random_uuid();
let image_size = get_aspect_ratio(request);
let num_images = match request.number_images {
None => None,
Some(1) => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::One),
Some(2) => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Two),
Some(3) => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Three),
Some(4) => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Four),
// TODO: Error
//Some(other) => {
// return Err(InternalImageError::InvalidNumberOfRequestedImages {
// min: 1,
// max: 4,
// requested: other,
// });
//},
_ => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::One), // Default to one image if invalid number
};
let request = BytedanceSeedreamV4MultiFunctionImageGenRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
num_images,
image_size,
// Not provided for text-to-image
image_media_tokens: None,
// Not provided
max_images: None,
};
let result = bytedance_seedream_v4_multi_function_image_gen(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft seedream 4. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft seedream 4: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::Seedream4p5),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
})
}
fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<BytedanceSeedreamV4MultiFunctionImageGenImageSize> {
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
// Handle modern aspect ratio
let aspect = aspect_ratio_to_artcraft_seedream_4(common_aspect_ratio);
return Some(aspect);
}
if let Some(aspect_ratio) = request.aspect_ratio {
// Handle deprecated aspect ratio
let aspect_ratio = match aspect_ratio {
TextToImageSize::Auto => BytedanceSeedreamV4MultiFunctionImageGenImageSize::Square,
TextToImageSize::Square => BytedanceSeedreamV4MultiFunctionImageGenImageSize::Square,
TextToImageSize::Tall => BytedanceSeedreamV4MultiFunctionImageGenImageSize::PortraitSixteenNine,
TextToImageSize::Wide => BytedanceSeedreamV4MultiFunctionImageGenImageSize::LandscapeSixteenNine,
};
return Some(aspect_ratio);
}
None
}
@@ -1,116 +0,0 @@
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_seedream_4p5::aspect_ratio_to_artcraft_seedream_4p5;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EnqueueEditImageCommand};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::{BytedanceSeedreamV4p5MultiFunctionImageGenImageSize, BytedanceSeedreamV4p5MultiFunctionImageGenNumImages, BytedanceSeedreamV4p5MultiFunctionImageGenRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use std::time::Duration;
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen_image::bytedance_seedream_v4p5_multi_function_image_gen;
use tauri::AppHandle;
pub async fn handle_artcraft_seedream_4p5_text_to_image(
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(GenerateError::needs_storyteller_credentials());
},
};
info!("Calling Artcraft seedream 4.5 ...");
let uuid_idempotency_token = generate_random_uuid();
let image_size = get_aspect_ratio(request);
let num_images = match request.number_images {
None => None,
Some(1) => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::One),
Some(2) => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Two),
Some(3) => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Three),
Some(4) => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Four),
// TODO: Error
//Some(other) => {
// return Err(InternalImageError::InvalidNumberOfRequestedImages {
// min: 1,
// max: 4,
// requested: other,
// });
//},
_ => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::One), // Default to one image if invalid number
};
let request = BytedanceSeedreamV4p5MultiFunctionImageGenRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
num_images,
image_size,
// Not provided for text-to-image
image_media_tokens: None,
// Not provided
max_images: None,
};
let result = bytedance_seedream_v4p5_multi_function_image_gen(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft seedream 4.5. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft seedream 4.5: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::Seedream4p5),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
})
}
fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<BytedanceSeedreamV4p5MultiFunctionImageGenImageSize> {
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
// Handle modern aspect ratio
let aspect = aspect_ratio_to_artcraft_seedream_4p5(common_aspect_ratio);
return Some(aspect);
}
if let Some(aspect_ratio) = request.aspect_ratio {
// Handle deprecated aspect ratio
let aspect_ratio = match aspect_ratio {
TextToImageSize::Auto => BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::Square,
TextToImageSize::Square => BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::Square,
TextToImageSize::Tall => BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::PortraitSixteenNine,
TextToImageSize::Wide => BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::LandscapeSixteenNine,
};
return Some(aspect_ratio);
}
None
}
@@ -8,8 +8,6 @@ use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_gpt
use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_gpt_image_1p5_text_to_image::handle_artcraft_gpt_image_1p5_text_to_image;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_text_to_image_artcraft_via_router::handle_text_to_image_artcraft_via_router;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_nano_banana_text_to_image::handle_artcraft_nano_banana_text_to_image;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_seedream_4_text_to_image::handle_artcraft_seedream_4_text_to_image;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_seedream_4p5_text_to_image::handle_artcraft_seedream_4p5_text_to_image;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageModel};
use crate::core::commands::enqueue::text_to_image::text_to_image_models::text_to_image_model_to_model_type;
use crate::core::events::generation_events::common::GenerationModel;
@@ -49,7 +47,7 @@ pub async fn handle_text_to_image_artcraft(
handle_artcraft_nano_banana_text_to_image(request, app_env_configs, storyteller_creds_manager).await
},
TextToImageModel::NanoBananaPro => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBananaPro, GenerationModel::NanoBananaPro).await,
TextToImageModel::Seedream4 => handle_artcraft_seedream_4_text_to_image(request, app_env_configs, storyteller_creds_manager).await,
TextToImageModel::Seedream4p5 => handle_artcraft_seedream_4p5_text_to_image(request, app_env_configs, storyteller_creds_manager).await,
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,
}
}
@@ -4,8 +4,6 @@ mod handle_text_to_image_artcraft_via_router;
mod handle_artcraft_gpt_image_1_text_to_image;
mod handle_artcraft_gpt_image_1p5_text_to_image;
mod handle_artcraft_nano_banana_text_to_image;
mod handle_artcraft_seedream_4_text_to_image;
mod handle_artcraft_seedream_4p5_text_to_image;
mod handle_artcraft_flux_pro_1p1_ultra_text_to_image;
mod handle_artcraft_flux_1_dev_text_to_image;
mod handle_artcraft_flux_pro_1p1_text_to_image;