artcraft router: seedance (#1153)

add multiple providers to artcraft router for seedance
This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-03-16 22:43:50 -04:00
1 parent 87296752a5
commit 7cd8c0299b
61 files changed
+1352 -167

No files matched your search

+4
View File
@@ -1,5 +1,9 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="Markdown">
<option name="RIGHT_MARGIN" value="100" />
<option name="WRAP_ON_TYPING" value="1" />
</codeStyleSettings>
<codeStyleSettings language="Rust">
<option name="RIGHT_MARGIN" value="180" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
Generated
+4
View File
@@ -788,6 +788,8 @@ dependencies = [
"artcraft_client",
"fal_client",
"log",
"muapi_client",
"reqwest 0.12.23",
"seedance2pro",
"serde",
"serde_derive",
@@ -795,9 +797,11 @@ dependencies = [
"test_data",
"tokens",
"tokio",
"url_utils",
"uuid 1.18.1",
"workspace-build-acceleration",
"world_labs_api",
"wreq",
]
[[package]]
@@ -19,14 +19,18 @@ workspace-build-acceleration.workspace = true
artcraft_api_defs.workspace = true
artcraft_client.workspace = true
fal_client.workspace = true
muapi_client.workspace = true
seedance2pro.workspace = true
tokens.workspace = true
url_utils.workspace = true
world_labs_api.workspace = true
# External
anyhow.workspace = true
log.workspace = true
reqwest.workspace = true
serde.workspace = true
wreq.workspace = true
serde_derive.workspace = true
tokio.workspace = true
uuid.workspace = true
@@ -6,4 +6,6 @@ use serde_derive::{Deserialize, Serialize};
pub enum Provider {
Artcraft,
Fal,
Muapi,
Seedance2Pro,
}
@@ -5,3 +5,5 @@ pub mod request_mismatch_mitigation_strategy;
pub mod router_artcraft_client;
pub mod router_client;
pub mod router_fal_client;
pub mod router_muapi_client;
pub mod router_seedance2pro_client;
@@ -1,10 +1,14 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_muapi_client::RouterMuapiClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::client_error::{ClientError, ClientType};
pub struct MultiRouterClient {
pub(crate) artcraft_client: Option<RouterArtcraftClient>,
pub(crate) fal_client: Option<RouterFalClient>,
pub(crate) muapi_client: Option<RouterMuapiClient>,
pub(crate) seedance2pro_client: Option<RouterSeedance2ProClient>,
}
impl MultiRouterClient {
@@ -17,4 +21,14 @@ impl MultiRouterClient {
self.fal_client.as_ref()
.ok_or(ClientError::ClientNotConfigured(ClientType::Fal))
}
pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> {
self.muapi_client.as_ref()
.ok_or(ClientError::ClientNotConfigured(ClientType::Muapi))
}
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ClientError> {
self.seedance2pro_client.as_ref()
.ok_or(ClientError::ClientNotConfigured(ClientType::Seedance2Pro))
}
}
@@ -1,10 +1,14 @@
use crate::client::multi_router_client::MultiRouterClient;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_muapi_client::RouterMuapiClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
pub struct MultiRouterClientBuilder {
artcraft_client: Option<RouterArtcraftClient>,
fal_client: Option<RouterFalClient>,
muapi_client: Option<RouterMuapiClient>,
seedance2pro_client: Option<RouterSeedance2ProClient>,
}
impl MultiRouterClientBuilder {
@@ -12,6 +16,8 @@ impl MultiRouterClientBuilder {
Self {
artcraft_client: None,
fal_client: None,
muapi_client: None,
seedance2pro_client: None,
}
}
@@ -25,10 +31,22 @@ impl MultiRouterClientBuilder {
self
}
pub fn set_muapi_client(mut self, client: RouterMuapiClient) -> Self {
self.muapi_client = Some(client);
self
}
pub fn set_seedance2pro_client(mut self, client: RouterSeedance2ProClient) -> Self {
self.seedance2pro_client = Some(client);
self
}
pub fn build(self) -> MultiRouterClient {
MultiRouterClient {
artcraft_client: self.artcraft_client,
fal_client: self.fal_client,
muapi_client: self.muapi_client,
seedance2pro_client: self.seedance2pro_client,
}
}
}
@@ -1,12 +1,16 @@
use crate::client::multi_router_client::MultiRouterClient;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_muapi_client::RouterMuapiClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::client_error::{ClientError, ClientType};
pub enum RouterClient {
Multi(MultiRouterClient),
Artcraft(RouterArtcraftClient),
Fal(RouterFalClient),
Muapi(RouterMuapiClient),
Seedance2Pro(RouterSeedance2ProClient),
}
impl RouterClient {
@@ -25,4 +29,20 @@ impl RouterClient {
_ => Err(ClientError::ClientNotConfigured(ClientType::Fal)),
}
}
pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> {
match self {
RouterClient::Muapi(client) => Ok(client),
RouterClient::Multi(multi) => multi.get_muapi_client_ref(),
_ => Err(ClientError::ClientNotConfigured(ClientType::Muapi)),
}
}
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ClientError> {
match self {
RouterClient::Seedance2Pro(client) => Ok(client),
RouterClient::Multi(multi) => multi.get_seedance2pro_client_ref(),
_ => Err(ClientError::ClientNotConfigured(ClientType::Seedance2Pro)),
}
}
}
@@ -0,0 +1,11 @@
use muapi_client::creds::muapi_session::MuapiSession;
pub struct RouterMuapiClient {
pub(crate) session: MuapiSession,
}
impl RouterMuapiClient {
pub fn new(session: MuapiSession) -> Self {
RouterMuapiClient { session }
}
}
@@ -0,0 +1,11 @@
use seedance2pro::creds::seedance2pro_session::Seedance2ProSession;
pub struct RouterSeedance2ProClient {
pub(crate) session: Seedance2ProSession,
}
impl RouterSeedance2ProClient {
pub fn new(session: Seedance2ProSession) -> Self {
RouterSeedance2ProClient { session }
}
}
@@ -1,4 +1,5 @@
use crate::errors::client_error::ClientError;
use crate::errors::download_error::DownloadError;
use crate::errors::provider_error::ProviderError;
use std::error::Error;
use std::fmt::{Display, Formatter};
@@ -8,6 +9,9 @@ pub enum ArtcraftRouterError {
/// A client configuration error.
Client(ClientError),
/// Failed to download a file from a URL (e.g. when re-uploading to a provider's CDN).
Download(DownloadError),
/// The requested model is not yet supported by the router.
UnsupportedModel(String),
@@ -24,6 +28,7 @@ impl Display for ArtcraftRouterError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Client(e) => write!(f, "Client error: {}", e),
Self::Download(e) => write!(f, "Download error: {}", e),
Self::UnsupportedModel(model) => write!(f, "Unsupported model: {}", model),
Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
Self::Provider(e) => write!(f, "Provider error: {}", e),
@@ -37,6 +42,12 @@ impl From<ClientError> for ArtcraftRouterError {
}
}
impl From<DownloadError> for ArtcraftRouterError {
fn from(error: DownloadError) -> Self {
Self::Download(error)
}
}
impl From<ProviderError> for ArtcraftRouterError {
fn from(error: ProviderError) -> Self {
Self::Provider(error)
@@ -5,6 +5,8 @@ use std::fmt::{Display, Formatter};
pub enum ClientType {
Artcraft,
Fal,
Muapi,
Seedance2Pro,
}
impl Display for ClientType {
@@ -12,6 +14,8 @@ impl Display for ClientType {
match self {
Self::Artcraft => write!(f, "Artcraft"),
Self::Fal => write!(f, "Fal"),
Self::Muapi => write!(f, "Muapi"),
Self::Seedance2Pro => write!(f, "Seedance2Pro"),
}
}
}
@@ -33,6 +37,12 @@ pub enum ClientError {
/// Fal only accepts image URLs for image inputs, not media tokens.
FalOnlySupportsUrls,
/// Muapi only accepts URLs for image inputs, not media tokens.
MuapiOnlySupportsUrls,
/// Seedance2Pro only accepts URLs for media inputs, not media tokens.
Seedance2ProOnlySupportsUrls,
}
impl Error for ClientError {}
@@ -55,6 +65,12 @@ impl Display for ClientError {
Self::FalOnlySupportsUrls => {
write!(f, "Fal only supports image URLs for image inputs, not media tokens")
}
Self::MuapiOnlySupportsUrls => {
write!(f, "Muapi only supports URLs for image inputs; resolve media tokens to URLs before calling this provider")
}
Self::Seedance2ProOnlySupportsUrls => {
write!(f, "Seedance2Pro only supports URLs for media inputs; resolve media tokens to URLs before calling this provider")
}
}
}
}
@@ -0,0 +1,40 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum DownloadError {
BadStatus {
url: String,
status_code: u16,
},
ReqwestDownload {
url: String,
error: reqwest::Error,
maybe_status: Option<reqwest::StatusCode>,
},
WreqDownload {
url: String,
error: wreq::Error,
maybe_status: Option<wreq::StatusCode>,
},
}
impl Error for DownloadError {}
impl Display for DownloadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::BadStatus { url, status_code } => {
write!(f, "Download failed for {} with status {}", url, status_code)
}
Self::ReqwestDownload { url, error, maybe_status } => {
write!(f, "Reqwest download error for {}: {} (status: {:?})", url, error, maybe_status)
}
Self::WreqDownload { url, error, maybe_status } => {
write!(f, "Wreq download error for {}: {} (status: {:?})", url, error, maybe_status)
}
}
}
}
@@ -1,3 +1,4 @@
pub mod artcraft_router_error;
pub mod client_error;
pub mod download_error;
pub mod provider_error;
@@ -2,11 +2,15 @@ use std::error::Error;
use std::fmt::{Display, Formatter};
use artcraft_client::error::storyteller_error::StorytellerError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use muapi_client::error::muapi_error::MuapiError;
use seedance2pro::error::seedance2pro_error::Seedance2ProError;
#[derive(Debug)]
pub enum ProviderError {
Storyteller(StorytellerError),
Fal(FalErrorPlus),
Muapi(MuapiError),
Seedance2Pro(Seedance2ProError),
}
impl Error for ProviderError {}
@@ -16,6 +20,8 @@ impl Display for ProviderError {
match self {
Self::Storyteller(e) => write!(f, "Storyteller provider error: {}", e),
Self::Fal(e) => write!(f, "Fal provider error: {}", e),
Self::Muapi(e) => write!(f, "Muapi provider error: {}", e),
Self::Seedance2Pro(e) => write!(f, "Seedance2Pro provider error: {}", e),
}
}
}
@@ -31,3 +37,15 @@ impl From<FalErrorPlus> for ProviderError {
Self::Fal(error)
}
}
impl From<MuapiError> for ProviderError {
fn from(error: MuapiError) -> Self {
Self::Muapi(error)
}
}
impl From<Seedance2ProError> for ProviderError {
fn from(error: Seedance2ProError) -> Self {
Self::Seedance2Pro(error)
}
}
@@ -73,72 +73,48 @@ impl<'a> GenerateImageRequest<'a> {
/// Read the image generation request, construct a plan, then yield a means to execute it.
pub fn build(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
match self.provider {
Provider::Artcraft => match self.model {
CommonImageModel::Flux1Dev => {
plan_generate_image_artcraft_flux_1_dev(self).map(ImageGenerationPlan::ArtcraftFlux1Dev)
}
CommonImageModel::Flux1Schnell => {
plan_generate_image_artcraft_flux_1_schnell(self).map(ImageGenerationPlan::ArtcraftFlux1Schnell)
}
CommonImageModel::FluxPro11 => {
plan_generate_image_artcraft_flux_pro_1p1(self).map(ImageGenerationPlan::ArtcraftFluxPro11)
}
CommonImageModel::FluxPro11Ultra => {
plan_generate_image_artcraft_flux_pro_1p1_ultra(self).map(ImageGenerationPlan::ArtcraftFluxPro11Ultra)
}
CommonImageModel::GptImage1p5 => {
plan_generate_image_artcraft_gpt_image_1p5(self).map(ImageGenerationPlan::ArtcraftGptImage1p5)
}
CommonImageModel::NanaBanana => {
plan_generate_image_artcraft_nano_banana(self).map(ImageGenerationPlan::ArtcraftNanaBanana)
}
CommonImageModel::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)
}
CommonImageModel::Seedream4 => {
plan_generate_image_artcraft_seedream_4(self).map(ImageGenerationPlan::ArtcraftSeedream4)
}
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)
}
CommonImageModel::QwenEdit2511Angles => {
plan_generate_image_artcraft_qwen_edit_2511_angles(self).map(ImageGenerationPlan::ArtcraftQwenEdit2511Angles)
}
CommonImageModel::Flux2LoraAngles => {
plan_generate_image_artcraft_flux_2_lora_angles(self).map(ImageGenerationPlan::ArtcraftFlux2LoraAngles)
}
},
Provider::Fal => match self.model {
CommonImageModel::NanaBananaPro => {
plan_generate_image_fal_nano_banana_pro(self).map(ImageGenerationPlan::FalNanaBananaPro)
}
CommonImageModel::Flux1Dev
| CommonImageModel::Flux1Schnell
| CommonImageModel::FluxPro11
| CommonImageModel::FluxPro11Ultra
| CommonImageModel::GptImage1p5
| CommonImageModel::NanaBanana
| CommonImageModel::NanaBanana2
| CommonImageModel::Seedream4
| CommonImageModel::Seedream4p5
| CommonImageModel::Seedream5Lite
| CommonImageModel::QwenEdit2511Angles
| CommonImageModel::Flux2LoraAngles => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "provider",
value: format!("{:?} is only available on the Artcraft provider", self.model),
}))
}
},
Provider::Artcraft => self.build_artcraft(),
Provider::Fal => self.build_fal(),
_ => self.unsupported_provider(),
}
}
fn build_artcraft(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
match self.model {
CommonImageModel::Flux1Dev => plan_generate_image_artcraft_flux_1_dev(self),
CommonImageModel::Flux1Schnell => plan_generate_image_artcraft_flux_1_schnell(self),
CommonImageModel::FluxPro11 => plan_generate_image_artcraft_flux_pro_1p1(self),
CommonImageModel::FluxPro11Ultra => plan_generate_image_artcraft_flux_pro_1p1_ultra(self),
CommonImageModel::GptImage1p5 => plan_generate_image_artcraft_gpt_image_1p5(self),
CommonImageModel::NanaBanana => plan_generate_image_artcraft_nano_banana(self),
CommonImageModel::NanaBanana2 => plan_generate_image_artcraft_nano_banana_2(self),
CommonImageModel::NanaBananaPro => plan_generate_image_artcraft_nano_banana_pro(self),
CommonImageModel::Seedream4 => plan_generate_image_artcraft_seedream_4(self),
CommonImageModel::Seedream4p5 => plan_generate_image_artcraft_seedream_4p5(self),
CommonImageModel::Seedream5Lite => plan_generate_image_artcraft_seedream_5_lite(self),
CommonImageModel::QwenEdit2511Angles => plan_generate_image_artcraft_qwen_edit_2511_angles(self),
CommonImageModel::Flux2LoraAngles => plan_generate_image_artcraft_flux_2_lora_angles(self),
}
}
fn build_fal(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
match self.model {
CommonImageModel::NanaBananaPro => plan_generate_image_fal_nano_banana_pro(self),
_ => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "provider",
value: format!("{:?} is only available on the Artcraft provider", self.model),
}))
}
}
}
fn unsupported_provider(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
Err(ArtcraftRouterError::UnsupportedModel(
format!("Image generation for model `{:?}` is not supported for provider {:?}", self.model, self.provider)
))
}
pub fn get_or_generate_idempotency_token(&self) -> String {
self.idempotency_token.map(|t| t.to_string())
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::{
GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageNumImages,
};
@@ -18,7 +19,7 @@ pub struct PlanArtcraftFlux1Dev<'a> {
pub fn plan_generate_image_artcraft_flux_1_dev<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftFlux1Dev<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
// Flux 1 Dev is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
if request.image_inputs.is_some() {
@@ -35,12 +36,12 @@ pub fn plan_generate_image_artcraft_flux_1_dev<'a>(
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftFlux1Dev {
Ok(ImageGenerationPlan::ArtcraftFlux1Dev(PlanArtcraftFlux1Dev {
prompt: request.prompt,
aspect_ratio,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
// Flux 1 Dev supported aspect ratios:
@@ -134,7 +135,6 @@ mod tests {
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_flux_1_dev_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::{
GenerateFlux1DevTextToImageAspectRatio as FlAr,
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::{
GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageNumImages,
};
@@ -18,7 +19,7 @@ pub struct PlanArtcraftFlux1Schnell<'a> {
pub fn plan_generate_image_artcraft_flux_1_schnell<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftFlux1Schnell<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
// Flux 1 Schnell is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
if request.image_inputs.is_some() {
@@ -35,12 +36,12 @@ pub fn plan_generate_image_artcraft_flux_1_schnell<'a>(
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftFlux1Schnell {
Ok(ImageGenerationPlan::ArtcraftFlux1Schnell(PlanArtcraftFlux1Schnell {
prompt: request.prompt,
aspect_ratio,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
// Flux 1 Schnell supported aspect ratios (identical to Flux 1 Dev):
@@ -134,7 +135,6 @@ mod tests {
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_flux_1_schnell_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::{
GenerateFlux1SchnellTextToImageAspectRatio as FlAr,
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
Flux2LoraEditImageAngleImageSize,
Flux2LoraEditImageAngleNumImages,
@@ -23,7 +24,7 @@ pub struct PlanArtcraftFlux2LoraAngles<'a> {
pub fn plan_generate_image_artcraft_flux_2_lora_angles<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftFlux2LoraAngles<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
// Angle models require exactly one input image.
@@ -31,7 +32,7 @@ pub fn plan_generate_image_artcraft_flux_2_lora_angles<'a>(
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftFlux2LoraAngles {
Ok(ImageGenerationPlan::ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles {
image_input,
horizontal_angle: request.horizontal_angle,
vertical_angle: request.vertical_angle,
@@ -39,7 +40,7 @@ pub fn plan_generate_image_artcraft_flux_2_lora_angles<'a>(
image_size,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_single_image_input<'a>(
@@ -139,7 +140,6 @@ fn plan_num_images(
mod tests {
use super::*;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_flux_2_lora_angles_image_request;
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
Flux2LoraEditImageAngleImageSize as S,
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::{
GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageNumImages,
};
@@ -18,7 +19,7 @@ pub struct PlanArtcraftFluxPro11<'a> {
pub fn plan_generate_image_artcraft_flux_pro_1p1<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftFluxPro11<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
// Flux Pro 1.1 is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
if request.image_inputs.is_some() {
@@ -35,12 +36,12 @@ pub fn plan_generate_image_artcraft_flux_pro_1p1<'a>(
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftFluxPro11 {
Ok(ImageGenerationPlan::ArtcraftFluxPro11(PlanArtcraftFluxPro11 {
prompt: request.prompt,
aspect_ratio,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
// Flux Pro 1.1 supported aspect ratios (no SquareHd):
@@ -134,7 +135,6 @@ mod tests {
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_flux_pro_1p1_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::{
GenerateFluxPro11TextToImageAspectRatio as FlAr,
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::{
GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageNumImages,
};
@@ -18,7 +19,7 @@ pub struct PlanArtcraftFluxPro11Ultra<'a> {
pub fn plan_generate_image_artcraft_flux_pro_1p1_ultra<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftFluxPro11Ultra<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
// Flux Pro 1.1 Ultra is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
if request.image_inputs.is_some() {
@@ -35,12 +36,12 @@ pub fn plan_generate_image_artcraft_flux_pro_1p1_ultra<'a>(
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftFluxPro11Ultra {
Ok(ImageGenerationPlan::ArtcraftFluxPro11Ultra(PlanArtcraftFluxPro11Ultra {
prompt: request.prompt,
aspect_ratio,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
// Flux Pro 1.1 Ultra supported aspect ratios:
@@ -137,7 +138,6 @@ mod tests {
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_flux_pro_1p1_ultra_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::{
GenerateFluxPro11UltraTextToImageAspectRatio as FlAr,
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{
GptImage1p5MultiFunctionImageGenNumImages, GptImage1p5MultiFunctionImageGenQuality,
GptImage1p5MultiFunctionImageGenSize,
@@ -24,21 +25,21 @@ pub struct PlanArtcraftGptImage1p5<'a> {
pub fn plan_generate_image_artcraft_gpt_image_1p5<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftGptImage1p5<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftGptImage1p5 {
Ok(ImageGenerationPlan::ArtcraftGptImage1p5(PlanArtcraftGptImage1p5 {
prompt: request.prompt,
image_inputs,
image_size,
quality: GptImage1p5MultiFunctionImageGenQuality::Medium,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_image_list_ref<'a>(
@@ -123,7 +124,6 @@ mod tests {
use crate::api::image_list_ref::ImageListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_gpt_image_1p5_image_request;
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{
GptImage1p5MultiFunctionImageGenNumImages as GN,
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::{
NanoBananaMultiFunctionImageGenAspectRatio, NanoBananaMultiFunctionImageGenNumImages,
};
@@ -21,7 +22,7 @@ pub struct PlanArtcraftNanaBanana<'a> {
pub fn plan_generate_image_artcraft_nano_banana<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftNanaBanana<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
@@ -29,13 +30,13 @@ pub fn plan_generate_image_artcraft_nano_banana<'a>(
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftNanaBanana {
Ok(ImageGenerationPlan::ArtcraftNanaBanana(PlanArtcraftNanaBanana {
prompt: request.prompt,
image_inputs,
aspect_ratio,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_image_list_ref<'a>(
@@ -148,7 +149,6 @@ mod tests {
use crate::api::image_list_ref::ImageListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_nano_banana_image_request;
use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::{
NanoBananaMultiFunctionImageGenAspectRatio as NbAr,
@@ -5,6 +5,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::{
NanaBanana2MultiFunctionImageGenAspectRatio, NanaBanana2MultiFunctionImageGenImageResolution,
NanaBanana2MultiFunctionImageGenNumImages,
@@ -24,7 +25,7 @@ pub struct PlanArtcraftNanaBanana2<'a> {
pub fn plan_generate_image_artcraft_nano_banana_2<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftNanaBanana2<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
@@ -33,14 +34,14 @@ pub fn plan_generate_image_artcraft_nano_banana_2<'a>(
let resolution = plan_resolution(request.resolution, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftNanaBanana2 {
Ok(ImageGenerationPlan::ArtcraftNanaBanana2(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>(
@@ -172,7 +173,6 @@ mod tests {
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,
@@ -5,6 +5,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{
NanoBananaProMultiFunctionImageGenAspectRatio, NanoBananaProMultiFunctionImageGenImageResolution,
NanoBananaProMultiFunctionImageGenNumImages,
@@ -24,7 +25,7 @@ pub struct PlanArtcraftNanaBananaPro<'a> {
pub fn plan_generate_image_artcraft_nano_banana_pro<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftNanaBananaPro<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
@@ -33,14 +34,14 @@ pub fn plan_generate_image_artcraft_nano_banana_pro<'a>(
let resolution = plan_resolution(request.resolution, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftNanaBananaPro {
Ok(ImageGenerationPlan::ArtcraftNanaBananaPro(PlanArtcraftNanaBananaPro {
prompt: request.prompt,
image_inputs,
aspect_ratio,
resolution,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_image_list_ref<'a>(
@@ -172,7 +173,6 @@ mod tests {
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_image_request;
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{
NanoBananaProMultiFunctionImageGenAspectRatio as NbpAr,
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::{
QwenEdit2511EditImageAngleImageSize,
QwenEdit2511EditImageAngleNumImages,
@@ -24,7 +25,7 @@ pub struct PlanArtcraftQwenEdit2511Angles<'a> {
pub fn plan_generate_image_artcraft_qwen_edit_2511_angles<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftQwenEdit2511Angles<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
// Angle models require exactly one input image.
@@ -32,7 +33,7 @@ pub fn plan_generate_image_artcraft_qwen_edit_2511_angles<'a>(
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftQwenEdit2511Angles {
Ok(ImageGenerationPlan::ArtcraftQwenEdit2511Angles(PlanArtcraftQwenEdit2511Angles {
prompt: request.prompt,
image_input,
horizontal_angle: request.horizontal_angle,
@@ -41,7 +42,7 @@ pub fn plan_generate_image_artcraft_qwen_edit_2511_angles<'a>(
image_size,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_single_image_input<'a>(
@@ -141,7 +142,6 @@ fn plan_num_images(
mod tests {
use super::*;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_qwen_edit_2511_angles_image_request;
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::{
QwenEdit2511EditImageAngleImageSize as S,
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::{
BytedanceSeedreamV4MultiFunctionImageGenImageSize,
BytedanceSeedreamV4MultiFunctionImageGenNumImages,
@@ -22,7 +23,7 @@ pub struct PlanArtcraftSeedream4<'a> {
pub fn plan_generate_image_artcraft_seedream_4<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftSeedream4<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
@@ -30,13 +31,13 @@ pub fn plan_generate_image_artcraft_seedream_4<'a>(
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 {
Ok(ImageGenerationPlan::ArtcraftSeedream4(PlanArtcraftSeedream4 {
prompt: request.prompt,
image_inputs,
image_size,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_image_list_ref<'a>(
@@ -147,7 +148,6 @@ mod tests {
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,
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::{
BytedanceSeedreamV4p5MultiFunctionImageGenImageSize,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages,
@@ -22,7 +23,7 @@ pub struct PlanArtcraftSeedream4p5<'a> {
pub fn plan_generate_image_artcraft_seedream_4p5<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftSeedream4p5<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
@@ -30,13 +31,13 @@ pub fn plan_generate_image_artcraft_seedream_4p5<'a>(
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 {
Ok(ImageGenerationPlan::ArtcraftSeedream4p5(PlanArtcraftSeedream4p5 {
prompt: request.prompt,
image_inputs,
image_size,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_image_list_ref<'a>(
@@ -147,7 +148,6 @@ mod tests {
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,
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::{
BytedanceSeedream5LiteMultiFunctionImageGenImageSize,
BytedanceSeedream5LiteMultiFunctionImageGenNumImages,
@@ -22,7 +23,7 @@ pub struct PlanArtcraftSeedream5Lite<'a> {
pub fn plan_generate_image_artcraft_seedream_5_lite<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftSeedream5Lite<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
@@ -30,13 +31,13 @@ pub fn plan_generate_image_artcraft_seedream_5_lite<'a>(
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 {
Ok(ImageGenerationPlan::ArtcraftSeedream5Lite(PlanArtcraftSeedream5Lite {
prompt: request.prompt,
image_inputs,
image_size,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_image_list_ref<'a>(
@@ -148,7 +149,6 @@ mod tests {
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,
@@ -5,6 +5,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::EnqueueNanoBananaProEditImageAspectRatio;
use fal_client::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_image_webhook::EnqueueNanoBananaProTextToImageAspectRatio;
@@ -44,7 +45,7 @@ pub struct PlanFalNanaBananaPro<'a> {
pub fn plan_generate_image_fal_nano_banana_pro<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanFalNanaBananaPro<'a>, ArtcraftRouterError> {
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
let image_urls = resolve_image_list_ref(request.image_inputs)?;
@@ -53,14 +54,14 @@ pub fn plan_generate_image_fal_nano_banana_pro<'a>(
let resolution = plan_resolution(request.resolution)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanFalNanaBananaPro {
Ok(ImageGenerationPlan::FalNanaBananaPro(PlanFalNanaBananaPro {
prompt: request.prompt,
image_urls,
t2i_aspect_ratio,
edit_aspect_ratio,
resolution,
num_images,
})
}))
}
fn resolve_image_list_ref(
@@ -263,7 +264,10 @@ mod tests {
fn build_plan<'a>(
request: &'a GenerateImageRequest<'a>,
) -> PlanFalNanaBananaPro<'a> {
plan_generate_image_fal_nano_banana_pro(request).expect("plan should succeed")
let ImageGenerationPlan::FalNanaBananaPro(plan) = plan_generate_image_fal_nano_banana_pro(request).expect("plan should succeed") else {
panic!("expected FalNanaBananaPro variant")
};
plan
}
// ── Image inputs / mode detection ────────────────────────────────────────────
@@ -28,20 +28,24 @@ impl<'a> GenerateSplatRequest<'a> {
/// Read the splat generation request, construct a plan, then yield a means to execute it.
pub fn build(&self) -> Result<SplatGenerationPlan<'_>, ArtcraftRouterError> {
match self.provider {
Provider::Artcraft => match self.model {
CommonSplatModel::Marble0p1Mini => {
plan_generate_splat_artcraft_marble_0p1_mini(self).map(SplatGenerationPlan::ArtcraftMarble0p1Mini)
}
CommonSplatModel::Marble0p1Plus => {
plan_generate_splat_artcraft_marble_0p1_plus(self).map(SplatGenerationPlan::ArtcraftMarble0p1Plus)
}
},
Provider::Fal => Err(ArtcraftRouterError::UnsupportedModel(
format!("Splat generation via Fal is not yet supported (model: {:?})", self.model)
)),
Provider::Artcraft => self.build_artcraft(),
_ => self.unsupported_provider(),
}
}
fn build_artcraft(&self) -> Result<SplatGenerationPlan<'_>, ArtcraftRouterError> {
match self.model {
CommonSplatModel::Marble0p1Mini => plan_generate_splat_artcraft_marble_0p1_mini(self),
CommonSplatModel::Marble0p1Plus => plan_generate_splat_artcraft_marble_0p1_plus(self),
}
}
fn unsupported_provider(&self) -> Result<SplatGenerationPlan<'_>, ArtcraftRouterError> {
Err(ArtcraftRouterError::UnsupportedModel(
format!("Splat generation for model `{:?}` is not supported for provider {:?}", self.model, self.provider)
))
}
pub fn get_or_generate_idempotency_token(&self) -> String {
self.idempotency_token.map(|t| t.to_string())
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
@@ -2,6 +2,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
use crate::generate::generate_splat::splat_generation_plan::SplatGenerationPlan;
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
@@ -13,14 +14,14 @@ pub struct PlanArtcraftMarble0p1Mini<'a> {
pub fn plan_generate_splat_artcraft_marble_0p1_mini<'a>(
request: &'a GenerateSplatRequest<'a>,
) -> Result<PlanArtcraftMarble0p1Mini<'a>, ArtcraftRouterError> {
) -> Result<SplatGenerationPlan<'a>, ArtcraftRouterError> {
let reference_image = resolve_single_image_ref(request.reference_images)?;
Ok(PlanArtcraftMarble0p1Mini {
Ok(SplatGenerationPlan::ArtcraftMarble0p1Mini(PlanArtcraftMarble0p1Mini {
prompt: request.prompt,
reference_image,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_single_image_ref<'a>(
@@ -2,6 +2,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
use crate::generate::generate_splat::splat_generation_plan::SplatGenerationPlan;
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
@@ -13,14 +14,14 @@ pub struct PlanArtcraftMarble0p1Plus<'a> {
pub fn plan_generate_splat_artcraft_marble_0p1_plus<'a>(
request: &'a GenerateSplatRequest<'a>,
) -> Result<PlanArtcraftMarble0p1Plus<'a>, ArtcraftRouterError> {
) -> Result<SplatGenerationPlan<'a>, ArtcraftRouterError> {
let reference_image = resolve_single_image_ref(request.reference_images)?;
Ok(PlanArtcraftMarble0p1Plus {
Ok(SplatGenerationPlan::ArtcraftMarble0p1Plus(PlanArtcraftMarble0p1Plus {
prompt: request.prompt,
reference_image,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_single_image_ref<'a>(
@@ -1 +1,3 @@
pub (crate) mod artcraft;
pub (crate) mod muapi;
pub (crate) mod seedance2pro;
@@ -0,0 +1,59 @@
use crate::generate::generate_video::plan::muapi::plan_generate_video_muapi_seedance2p0::{
MuapiSeedance2p0Mode, PlanMuapiSeedance2p0,
};
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use muapi_client::creds::muapi_api_key::MuapiApiKey;
use muapi_client::creds::muapi_session::MuapiSession;
use muapi_client::requests::seedance_2p0_image_to_video::seedance_2p0_image_to_video::Seedance2p0ImageToVideoArgs;
use muapi_client::requests::seedance_2p0_text_to_video::seedance_2p0_text_to_video::Seedance2p0TextToVideoArgs;
pub(crate) fn estimate_video_cost_muapi_seedance2p0(
plan: &PlanMuapiSeedance2p0,
) -> VideoGenerationCostEstimate {
// A dummy session is sufficient — cost estimation does not make any network calls.
let dummy_session = MuapiSession::new(MuapiApiKey::new(String::new()));
let cost_in_usd_cents;
let cost_in_credits;
match &plan.mode {
MuapiSeedance2p0Mode::TextToVideo { aspect_ratio, duration, quality } => {
let args = Seedance2p0TextToVideoArgs {
session: &dummy_session,
prompt: String::new(),
aspect_ratio: *aspect_ratio,
duration: *duration,
quality: *quality,
};
cost_in_usd_cents = args.estimate_cost_in_usd_cents();
// NB: This is off by a factor of 100 because they use a fractional system.
cost_in_credits = (args.estimate_credits() * 100.0f64).round() as u64;
}
MuapiSeedance2p0Mode::ImageToVideo { aspect_ratio, duration, quality, .. } => {
let args = Seedance2p0ImageToVideoArgs {
session: &dummy_session,
prompt: String::new(),
image_urls: vec![],
aspect_ratio: *aspect_ratio,
duration: *duration,
quality: *quality,
};
cost_in_usd_cents = args.estimate_cost_in_usd_cents();
// NB: This is off by a factor of 100 because they use a fractional system.
cost_in_credits = (args.estimate_credits() * 100.0f64).round() as u64;
}
}
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_credits),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
@@ -0,0 +1 @@
pub mod estimate_video_cost_muapi_seedance2p0;
@@ -0,0 +1,38 @@
use seedance2pro::creds::seedance2pro_session::Seedance2ProSession;
use seedance2pro::requests::generate_video::generate_video::{GenerateVideoArgs, Resolution};
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::PlanSeedance2proSeedance2p0;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
pub(crate) fn estimate_video_cost_seedance2pro_seedance2p0(
plan: &PlanSeedance2proSeedance2p0,
) -> VideoGenerationCostEstimate {
// A dummy session is sufficient — cost estimation does not make any network calls.
let dummy_session = Seedance2ProSession::from_cookies_string(String::new());
let args = GenerateVideoArgs {
session: &dummy_session,
prompt: String::new(),
resolution: Resolution::Square1x1, // Resolution does not affect cost
duration_seconds: plan.duration_seconds,
batch_count: plan.batch_count,
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
reference_video_urls: None,
reference_audio_urls: None,
use_face_blur_hack: None,
};
let cost_in_credits = args.estimate_credits();
let cost_in_usd_cents = args.estimate_cost_in_usd_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_credits as u64),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
@@ -0,0 +1 @@
pub mod estimate_video_cost_seedance2pro_seedance2p0;
@@ -1 +1,3 @@
pub mod artcraft;
pub mod muapi;
pub mod seedance2pro;
@@ -0,0 +1,56 @@
use crate::client::router_muapi_client::RouterMuapiClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_video::generate_video_response::{
GenerateVideoResponse, MuapiVideoResponsePayload,
};
use crate::generate::generate_video::plan::muapi::plan_generate_video_muapi_seedance2p0::{
MuapiSeedance2p0Mode, PlanMuapiSeedance2p0,
};
use muapi_client::requests::seedance_2p0_image_to_video::seedance_2p0_image_to_video::{
Seedance2p0ImageToVideoArgs, seedance_2p0_image_to_video,
};
use muapi_client::requests::seedance_2p0_text_to_video::seedance_2p0_text_to_video::{
Seedance2p0TextToVideoArgs, seedance_2p0_text_to_video,
};
pub async fn execute_muapi_seedance2p0(
plan: &PlanMuapiSeedance2p0,
muapi_client: &RouterMuapiClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let session = &muapi_client.session;
let request_id = match &plan.mode {
MuapiSeedance2p0Mode::TextToVideo { aspect_ratio, duration, quality } => {
let args = Seedance2p0TextToVideoArgs {
session,
prompt: plan.prompt.clone(),
aspect_ratio: *aspect_ratio,
duration: *duration,
quality: *quality,
};
let response = seedance_2p0_text_to_video(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Muapi(err)))?;
response.request_id
}
MuapiSeedance2p0Mode::ImageToVideo { image_urls, aspect_ratio, duration, quality } => {
let args = Seedance2p0ImageToVideoArgs {
session,
prompt: plan.prompt.clone(),
image_urls: image_urls.clone(),
aspect_ratio: *aspect_ratio,
duration: *duration,
quality: *quality,
};
let response = seedance_2p0_image_to_video(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Muapi(err)))?;
response.request_id
}
};
Ok(GenerateVideoResponse::Muapi(MuapiVideoResponsePayload {
request_id,
}))
}
@@ -0,0 +1 @@
pub mod execute_muapi_seedance2p0;
@@ -0,0 +1,112 @@
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_video::generate_video_response::{
GenerateVideoResponse, Seedance2proVideoResponsePayload,
};
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::PlanSeedance2proSeedance2p0;
use crate::utils::download_file::download_file;
use seedance2pro::requests::generate_video::generate_video::{
generate_video, GenerateVideoArgs,
};
use seedance2pro::requests::prepare_file_upload::prepare_file_upload::{
prepare_file_upload, PrepareFileUploadArgs,
};
use seedance2pro::requests::upload_file::upload_file::{
upload_file, UploadFileArgs,
};
use url_utils::extension::extract_extension_from_url::{
extract_extension_from_url_str, ExtractExtensions,
};
pub async fn execute_seedance2pro_seedance2p0(
plan: &PlanSeedance2proSeedance2p0,
seedance2pro_client: &RouterSeedance2ProClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let session = &seedance2pro_client.session;
// Upload media files to seedance2pro CDN
let start_frame_url = upload_optional_url(session, plan.start_frame_url.as_deref()).await?;
let end_frame_url = upload_optional_url(session, plan.end_frame_url.as_deref()).await?;
let reference_image_urls = upload_optional_url_list(session, plan.reference_image_urls.as_deref()).await?;
let reference_video_urls = upload_optional_url_list(session, plan.reference_video_urls.as_deref()).await?;
let reference_audio_urls = upload_optional_url_list(session, plan.reference_audio_urls.as_deref()).await?;
let args = GenerateVideoArgs {
session,
prompt: plan.prompt.clone().unwrap_or_default(),
resolution: plan.resolution,
duration_seconds: plan.duration_seconds,
batch_count: plan.batch_count,
start_frame_url,
end_frame_url,
reference_image_urls,
reference_video_urls,
reference_audio_urls,
use_face_blur_hack: None,
};
let response = generate_video(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
}))
}
/// Downloads a file from a source URL and re-uploads it to seedance2pro CDN.
async fn upload_to_seedance2pro(
session: &seedance2pro::creds::seedance2pro_session::Seedance2ProSession,
source_url: &str,
) -> Result<String, ArtcraftRouterError> {
let extension = extract_extension_from_url_str(source_url, &ExtractExtensions::All)
.map(|ext| ext.without_period().to_string())
.unwrap_or_else(|| "jpg".to_string());
let file_bytes = download_file(source_url).await?;
let prepare_response = prepare_file_upload(PrepareFileUploadArgs {
session,
extension,
})
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
let upload_response = upload_file(UploadFileArgs {
upload_url: prepare_response.upload_url,
file_bytes,
})
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(upload_response.public_url)
}
async fn upload_optional_url(
session: &seedance2pro::creds::seedance2pro_session::Seedance2ProSession,
url: Option<&str>,
) -> Result<Option<String>, ArtcraftRouterError> {
match url {
None => Ok(None),
Some(u) => Ok(Some(upload_to_seedance2pro(session, u).await?)),
}
}
async fn upload_optional_url_list(
session: &seedance2pro::creds::seedance2pro_session::Seedance2ProSession,
urls: Option<&[String]>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
match urls {
None => Ok(None),
Some(url_list) if url_list.is_empty() => Ok(None),
Some(url_list) => {
let mut uploaded = Vec::with_capacity(url_list.len());
for url in url_list {
uploaded.push(upload_to_seedance2pro(session, url).await?);
}
Ok(Some(uploaded))
}
}
}
@@ -0,0 +1 @@
pub mod execute_seedance2pro_seedance2p0;
@@ -12,8 +12,13 @@ use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraf
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::plan_generate_video_artcraft_kling3p0_standard;
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance1p5_pro::plan_generate_video_artcraft_seedance1p5_pro;
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::plan_generate_video_artcraft_seedance2p0;
use crate::generate::generate_video::plan::muapi::plan_generate_video_muapi_seedance2p0::plan_generate_video_muapi_seedance2p0;
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::plan_generate_video_seedance2pro_seedance2p0;
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
/// Plan to either (1) generate a video or (2) determine how much it costs to generate that video.
/// This works across multiple providers by shaping a generic "GenerateVideoRequest" into a provider-specific plan.
/// That plan can then be used to return a cost estimate for that given provider or return a struct that can be used to send a real generation request.
pub struct GenerateVideoRequest<'a> {
/// Which model to use.
pub model: CommonVideoModel,
@@ -72,27 +77,43 @@ impl<'a> GenerateVideoRequest<'a> {
/// Read the video generation request, construct a plan, then yield a means to execute it.
pub fn build(&self) -> Result<VideoGenerationPlan<'_>, ArtcraftRouterError> {
match self.provider {
Provider::Artcraft => match self.model {
CommonVideoModel::Kling3p0Pro => {
plan_generate_video_artcraft_kling3p0_pro(self).map(VideoGenerationPlan::ArtcraftKling3p0Pro)
}
CommonVideoModel::Kling3p0Standard => {
plan_generate_video_artcraft_kling3p0_standard(self).map(VideoGenerationPlan::ArtcraftKling3p0Standard)
}
CommonVideoModel::Seedance1p5Pro => {
plan_generate_video_artcraft_seedance1p5_pro(self).map(VideoGenerationPlan::ArtcraftSeedance1p5Pro)
}
CommonVideoModel::Seedance2p0 => {
plan_generate_video_artcraft_seedance2p0(self).map(VideoGenerationPlan::ArtcraftSeedance2p0)
}
_ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))),
},
Provider::Fal => Err(ArtcraftRouterError::UnsupportedModel(
format!("Video generation via Fal is not yet supported (model: {:?})", self.model)
)),
Provider::Artcraft => self.build_artcraft(),
Provider::Muapi => self.build_muapi(),
Provider::Seedance2Pro => self.build_seedance2pro(),
_ => self.unsupported_provider(),
}
}
fn build_artcraft(&self) -> Result<VideoGenerationPlan<'_>, ArtcraftRouterError> {
match self.model {
CommonVideoModel::Kling3p0Pro => plan_generate_video_artcraft_kling3p0_pro(self),
CommonVideoModel::Kling3p0Standard => plan_generate_video_artcraft_kling3p0_standard(self),
CommonVideoModel::Seedance1p5Pro => plan_generate_video_artcraft_seedance1p5_pro(self),
CommonVideoModel::Seedance2p0 => plan_generate_video_artcraft_seedance2p0(self),
_ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))),
}
}
fn build_muapi(&self) -> Result<VideoGenerationPlan<'_>, ArtcraftRouterError> {
match self.model {
CommonVideoModel::Seedance2p0 => plan_generate_video_muapi_seedance2p0(self),
_ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))),
}
}
fn build_seedance2pro(&self) -> Result<VideoGenerationPlan<'_>, ArtcraftRouterError> {
match self.model {
CommonVideoModel::Seedance2p0 => plan_generate_video_seedance2pro_seedance2p0(self),
_ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))),
}
}
fn unsupported_provider(&self) -> Result<VideoGenerationPlan<'_>, ArtcraftRouterError> {
Err(ArtcraftRouterError::UnsupportedModel(
format!("Video generation for model `{:?}` is not supported for provider {:?}", self.model, self.provider)
))
}
pub fn get_or_generate_idempotency_token(&self) -> String {
self.idempotency_token.map(|t| t.to_string())
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
@@ -1,3 +1,4 @@
use muapi_client::api_types::request_id::RequestId as MuapiRequestId;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
#[derive(Clone, Debug)]
@@ -6,15 +7,43 @@ pub struct ArtcraftVideoResponsePayload {
pub all_inference_job_tokens: Vec<InferenceJobToken>,
}
#[derive(Clone, Debug)]
pub struct MuapiVideoResponsePayload {
pub request_id: MuapiRequestId,
}
#[derive(Clone, Debug)]
pub struct Seedance2proVideoResponsePayload {
pub order_id: String,
pub task_id: String,
}
#[derive(Clone, Debug)]
pub enum GenerateVideoResponse {
Artcraft(ArtcraftVideoResponsePayload),
Muapi(MuapiVideoResponsePayload),
Seedance2Pro(Seedance2proVideoResponsePayload),
}
impl GenerateVideoResponse {
pub fn get_artcraft_payload(&self) -> Option<ArtcraftVideoResponsePayload> {
match self {
Self::Artcraft(p) => Some(p.clone()),
_ => None,
}
}
pub fn get_muapi_payload(&self) -> Option<MuapiVideoResponsePayload> {
match self {
Self::Muapi(p) => Some(p.clone()),
_ => None,
}
}
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proVideoResponsePayload> {
match self {
Self::Seedance2Pro(p) => Some(p.clone()),
_ => None,
}
}
}
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::{
Kling3p0ProMultiFunctionVideoGenAspectRatio,
Kling3p0ProMultiFunctionVideoGenDuration,
@@ -24,7 +25,7 @@ pub struct PlanArtcraftKling3p0Pro<'a> {
pub fn plan_generate_video_artcraft_kling3p0_pro<'a>(
request: &'a GenerateVideoRequest<'a>,
) -> Result<PlanArtcraftKling3p0Pro<'a>, ArtcraftRouterError> {
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let start_frame = resolve_image_ref(request.start_frame)?;
@@ -33,7 +34,7 @@ pub fn plan_generate_video_artcraft_kling3p0_pro<'a>(
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let duration = plan_duration(request.duration_seconds, strategy)?;
Ok(PlanArtcraftKling3p0Pro {
Ok(VideoGenerationPlan::ArtcraftKling3p0Pro(PlanArtcraftKling3p0Pro {
prompt: request.prompt,
negative_prompt: request.negative_prompt,
start_frame,
@@ -42,7 +43,7 @@ pub fn plan_generate_video_artcraft_kling3p0_pro<'a>(
duration,
generate_audio: request.generate_audio,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_image_ref<'a>(
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::{
Kling3p0StandardMultiFunctionVideoGenAspectRatio,
Kling3p0StandardMultiFunctionVideoGenDuration,
@@ -24,7 +25,7 @@ pub struct PlanArtcraftKling3p0Standard<'a> {
pub fn plan_generate_video_artcraft_kling3p0_standard<'a>(
request: &'a GenerateVideoRequest<'a>,
) -> Result<PlanArtcraftKling3p0Standard<'a>, ArtcraftRouterError> {
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let start_frame = resolve_image_ref(request.start_frame)?;
@@ -33,7 +34,7 @@ pub fn plan_generate_video_artcraft_kling3p0_standard<'a>(
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let duration = plan_duration(request.duration_seconds, strategy)?;
Ok(PlanArtcraftKling3p0Standard {
Ok(VideoGenerationPlan::ArtcraftKling3p0Standard(PlanArtcraftKling3p0Standard {
prompt: request.prompt,
negative_prompt: request.negative_prompt,
start_frame,
@@ -42,7 +43,7 @@ pub fn plan_generate_video_artcraft_kling3p0_standard<'a>(
duration,
generate_audio: request.generate_audio,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_image_ref<'a>(
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
use artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::{
Seedance1p5ProMultiFunctionVideoGenAspectRatio,
Seedance1p5ProMultiFunctionVideoGenDuration,
@@ -25,7 +26,7 @@ pub struct PlanArtcraftSeedance1p5Pro<'a> {
pub fn plan_generate_video_artcraft_seedance1p5_pro<'a>(
request: &'a GenerateVideoRequest<'a>,
) -> Result<PlanArtcraftSeedance1p5Pro<'a>, ArtcraftRouterError> {
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let start_frame = resolve_image_ref(request.start_frame)?;
@@ -35,7 +36,7 @@ pub fn plan_generate_video_artcraft_seedance1p5_pro<'a>(
let duration = plan_duration(request.duration_seconds, strategy)?;
let resolution = plan_resolution(request.resolution, strategy)?;
Ok(PlanArtcraftSeedance1p5Pro {
Ok(VideoGenerationPlan::ArtcraftSeedance1p5Pro(PlanArtcraftSeedance1p5Pro {
prompt: request.prompt,
start_frame,
end_frame,
@@ -44,7 +45,7 @@ pub fn plan_generate_video_artcraft_seedance1p5_pro<'a>(
resolution,
generate_audio: request.generate_audio,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_image_ref<'a>(
@@ -7,6 +7,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
Seedance2p0AspectRatio, Seedance2p0BatchCount,
};
@@ -28,7 +29,7 @@ pub struct PlanArtcraftSeedance2p0<'a> {
pub fn plan_generate_video_artcraft_seedance2p0<'a>(
request: &'a GenerateVideoRequest<'a>,
) -> Result<PlanArtcraftSeedance2p0<'a>, ArtcraftRouterError> {
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let start_frame = resolve_image_ref(request.start_frame)?;
@@ -41,7 +42,7 @@ pub fn plan_generate_video_artcraft_seedance2p0<'a>(
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
let duration_seconds = plan_duration(request.duration_seconds, strategy)?;
Ok(PlanArtcraftSeedance2p0 {
Ok(VideoGenerationPlan::ArtcraftSeedance2p0(PlanArtcraftSeedance2p0 {
prompt: request.prompt,
start_frame,
end_frame,
@@ -52,7 +53,7 @@ pub fn plan_generate_video_artcraft_seedance2p0<'a>(
duration_seconds,
batch_count,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}))
}
fn resolve_image_ref<'a>(
@@ -1 +1,3 @@
pub mod artcraft;
pub mod artcraft;
pub mod muapi;
pub mod seedance2pro;
@@ -0,0 +1 @@
pub mod plan_generate_video_muapi_seedance2p0;
@@ -0,0 +1,259 @@
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_video::generate_video_request::GenerateVideoRequest;
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
use muapi_client::requests::seedance_2p0_image_to_video::seedance_2p0_image_to_video::{
AspectRatio as I2vAspectRatio,
Duration as I2vDuration,
Quality as I2vQuality,
};
use muapi_client::requests::seedance_2p0_text_to_video::seedance_2p0_text_to_video::{
AspectRatio as T2vAspectRatio,
Duration as T2vDuration,
Quality as T2vQuality,
};
/// The generation mode determined at plan time.
/// Muapi has separate text-to-video and image-to-video endpoints.
/// If reference images are present, we use image-to-video; otherwise text-to-video.
#[derive(Debug, Clone)]
pub enum MuapiSeedance2p0Mode {
TextToVideo {
aspect_ratio: T2vAspectRatio,
duration: T2vDuration,
quality: T2vQuality,
},
ImageToVideo {
image_urls: Vec<String>,
aspect_ratio: I2vAspectRatio,
duration: I2vDuration,
quality: I2vQuality,
},
}
#[derive(Debug, Clone)]
pub struct PlanMuapiSeedance2p0 {
pub prompt: String,
pub mode: MuapiSeedance2p0Mode,
}
pub fn plan_generate_video_muapi_seedance2p0<'a>(
request: &'a GenerateVideoRequest<'a>,
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
// Muapi does not support start/end frames, video references, or audio references.
if request.start_frame.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "start_frame",
value: "Muapi does not support start frame keyframes".to_string(),
}));
}
if request.end_frame.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "end_frame",
value: "Muapi does not support end frame keyframes".to_string(),
}));
}
if request.reference_videos.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "reference_videos",
value: "Muapi does not support video references".to_string(),
}));
}
if request.reference_audio.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "reference_audio",
value: "Muapi does not support audio references".to_string(),
}));
}
let image_urls = resolve_image_urls(request.reference_images)?;
let prompt = request.prompt.unwrap_or("").to_string();
let mode = match image_urls {
Some(urls) => {
MuapiSeedance2p0Mode::ImageToVideo {
image_urls: urls,
aspect_ratio: plan_i2v_aspect_ratio(request.aspect_ratio, strategy)?,
duration: plan_i2v_duration(request.duration_seconds, strategy)?,
quality: I2vQuality::High, // NB: "Low" is a "fast" mode, I think
}
}
None => {
MuapiSeedance2p0Mode::TextToVideo {
aspect_ratio: plan_t2v_aspect_ratio(request.aspect_ratio, strategy)?,
duration: plan_t2v_duration(request.duration_seconds, strategy)?,
quality: T2vQuality::High, // NB: "Low" is a "fast" mode, I think
}
}
};
Ok(VideoGenerationPlan::MuapiSeedance2p0(PlanMuapiSeedance2p0 {
prompt,
mode,
}))
}
fn resolve_image_urls(
reference_images: Option<ImageListRef<'_>>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
match reference_images {
None => Ok(None),
Some(ImageListRef::Urls(urls)) => {
if urls.is_empty() {
Ok(None)
} else {
Ok(Some(urls.clone()))
}
}
Some(ImageListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::MuapiOnlySupportsUrls))
}
}
}
// Muapi supports: 16:9, 9:16, 4:3, 3:4 (no square, no 1:1)
fn plan_t2v_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<T2vAspectRatio, ArtcraftRouterError> {
match aspect_ratio {
None
| Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(T2vAspectRatio::Landscape16x9),
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
Ok(T2vAspectRatio::Landscape16x9)
}
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
Ok(T2vAspectRatio::Portrait9x16)
}
Some(CommonAspectRatio::WideFourByThree) => Ok(T2vAspectRatio::Standard4x3),
Some(CommonAspectRatio::TallThreeByFour) => Ok(T2vAspectRatio::Portrait3x4),
// Square and others are not supported by Muapi
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(nearest_t2v_aspect_ratio(unsupported))
}
},
}
}
fn nearest_t2v_aspect_ratio(aspect_ratio: CommonAspectRatio) -> T2vAspectRatio {
match aspect_ratio {
// Wide ratios → nearest wide
CommonAspectRatio::Square | CommonAspectRatio::SquareHd
| CommonAspectRatio::WideFiveByFour => T2vAspectRatio::Standard4x3,
CommonAspectRatio::WideThreeByTwo => T2vAspectRatio::Landscape16x9,
CommonAspectRatio::WideTwentyOneByNine => T2vAspectRatio::Landscape16x9,
// Tall ratios → nearest tall
CommonAspectRatio::TallFourByFive => T2vAspectRatio::Portrait3x4,
CommonAspectRatio::TallTwoByThree => T2vAspectRatio::Portrait9x16,
CommonAspectRatio::TallNineByTwentyOne => T2vAspectRatio::Portrait9x16,
_ => T2vAspectRatio::Landscape16x9,
}
}
fn plan_i2v_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<I2vAspectRatio, ArtcraftRouterError> {
match aspect_ratio {
None
| Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(I2vAspectRatio::Landscape16x9),
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
Ok(I2vAspectRatio::Landscape16x9)
}
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
Ok(I2vAspectRatio::Portrait9x16)
}
Some(CommonAspectRatio::WideFourByThree) => Ok(I2vAspectRatio::Standard4x3),
Some(CommonAspectRatio::TallThreeByFour) => Ok(I2vAspectRatio::Portrait3x4),
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(nearest_i2v_aspect_ratio(unsupported))
}
},
}
}
fn nearest_i2v_aspect_ratio(aspect_ratio: CommonAspectRatio) -> I2vAspectRatio {
match aspect_ratio {
CommonAspectRatio::Square | CommonAspectRatio::SquareHd
| CommonAspectRatio::WideFiveByFour => I2vAspectRatio::Standard4x3,
CommonAspectRatio::WideThreeByTwo => I2vAspectRatio::Landscape16x9,
CommonAspectRatio::WideTwentyOneByNine => I2vAspectRatio::Landscape16x9,
CommonAspectRatio::TallFourByFive => I2vAspectRatio::Portrait3x4,
CommonAspectRatio::TallTwoByThree => I2vAspectRatio::Portrait9x16,
CommonAspectRatio::TallNineByTwentyOne => I2vAspectRatio::Portrait9x16,
_ => I2vAspectRatio::Landscape16x9,
}
}
// Muapi supports durations of 5, 10, and 15 seconds.
fn plan_t2v_duration(
duration_seconds: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<T2vDuration, ArtcraftRouterError> {
let d = duration_seconds.unwrap_or(5);
match d {
0..=5 => Ok(T2vDuration::FiveSeconds),
6..=10 => Ok(T2vDuration::TenSeconds),
11..=15 => Ok(T2vDuration::FifteenSeconds),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "duration_seconds",
value: format!("{}", d),
}))
}
_ => Ok(T2vDuration::FifteenSeconds), // clamp to max
},
}
}
fn plan_i2v_duration(
duration_seconds: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<I2vDuration, ArtcraftRouterError> {
let d = duration_seconds.unwrap_or(5);
match d {
0..=5 => Ok(I2vDuration::FiveSeconds),
6..=10 => Ok(I2vDuration::TenSeconds),
11..=15 => Ok(I2vDuration::FifteenSeconds),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "duration_seconds",
value: format!("{}", d),
}))
}
_ => Ok(I2vDuration::FifteenSeconds),
},
}
}
@@ -0,0 +1 @@
pub mod plan_generate_video_seedance2pro_seedance2p0;
@@ -0,0 +1,209 @@
use crate::api::audio_list_ref::AudioListRef;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
use crate::api::video_list_ref::VideoListRef;
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_video::generate_video_request::GenerateVideoRequest;
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
use seedance2pro::requests::generate_video::generate_video::{BatchCount, Resolution};
#[derive(Debug, Clone)]
pub struct PlanSeedance2proSeedance2p0 {
pub prompt: Option<String>,
pub start_frame_url: Option<String>,
pub end_frame_url: Option<String>,
pub reference_image_urls: Option<Vec<String>>,
pub reference_video_urls: Option<Vec<String>>,
pub reference_audio_urls: Option<Vec<String>>,
pub resolution: Resolution,
pub duration_seconds: u8,
pub batch_count: BatchCount,
}
pub fn plan_generate_video_seedance2pro_seedance2p0<'a>(
request: &'a GenerateVideoRequest<'a>,
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let start_frame_url = resolve_image_ref_url(request.start_frame)?;
let end_frame_url = resolve_image_ref_url(request.end_frame)?;
let reference_image_urls = resolve_image_list_ref_urls(request.reference_images)?;
let reference_video_urls = resolve_video_list_ref_urls(request.reference_videos)?;
let reference_audio_urls = resolve_audio_list_ref_urls(request.reference_audio)?;
let resolution = plan_resolution(request.aspect_ratio, strategy)?;
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
let duration_seconds = plan_duration(request.duration_seconds, strategy)?;
Ok(VideoGenerationPlan::Seedance2proSeedance2p0(PlanSeedance2proSeedance2p0 {
prompt: request.prompt.map(|p| p.to_string()),
start_frame_url,
end_frame_url,
reference_image_urls,
reference_video_urls,
reference_audio_urls,
resolution,
duration_seconds,
batch_count,
}))
}
fn resolve_image_ref_url(
image_ref: Option<ImageRef<'_>>,
) -> Result<Option<String>, ArtcraftRouterError> {
match image_ref {
None => Ok(None),
Some(ImageRef::Url(url)) => Ok(Some(url.to_string())),
Some(ImageRef::MediaFileToken(_)) => {
Err(ArtcraftRouterError::Client(ClientError::Seedance2ProOnlySupportsUrls))
}
}
}
fn resolve_image_list_ref_urls(
image_list_ref: Option<ImageListRef<'_>>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
match image_list_ref {
None => Ok(None),
Some(ImageListRef::Urls(urls)) => Ok(Some(urls.clone())),
Some(ImageListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::Seedance2ProOnlySupportsUrls))
}
}
}
fn resolve_video_list_ref_urls(
video_list_ref: Option<VideoListRef<'_>>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
match video_list_ref {
None => Ok(None),
Some(VideoListRef::Urls(urls)) => Ok(Some(urls.clone())),
Some(VideoListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::Seedance2ProOnlySupportsUrls))
}
}
}
fn resolve_audio_list_ref_urls(
audio_list_ref: Option<AudioListRef<'_>>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
match audio_list_ref {
None => Ok(None),
Some(AudioListRef::Urls(urls)) => Ok(Some(urls.clone())),
Some(AudioListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::Seedance2ProOnlySupportsUrls))
}
}
}
// Supported aspect ratios and their AR values (width / height):
// Portrait9x16 = 0.5625, Portrait3x4 = 0.75, Square1x1 = 1.0, Standard4x3 = 1.33, Landscape16x9 = 1.78
//
// All supported ratios cost the same, so PayMoreUpgrade and PayLessDowngrade both
// select the nearest match rather than rounding in a specific direction.
fn plan_resolution(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Resolution, ArtcraftRouterError> {
match aspect_ratio {
// No preference or auto — default to landscape
None
| Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(Resolution::Landscape16x9),
// Direct mappings
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
Ok(Resolution::Landscape16x9)
}
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
Ok(Resolution::Portrait9x16)
}
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
Ok(Resolution::Square1x1)
}
Some(CommonAspectRatio::WideFourByThree) => Ok(Resolution::Standard4x3),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Resolution::Portrait3x4),
// Mismatches — apply strategy
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(nearest_resolution(unsupported))
}
},
}
}
/// Pick the nearest supported resolution by AR value (width / height).
fn nearest_resolution(aspect_ratio: CommonAspectRatio) -> Resolution {
match aspect_ratio {
CommonAspectRatio::WideFiveByFour => Resolution::Standard4x3, // 1.25, nearest 1.33
CommonAspectRatio::WideThreeByTwo => Resolution::Standard4x3, // 1.50, nearest 1.33
CommonAspectRatio::WideTwentyOneByNine => Resolution::Landscape16x9, // 2.33, nearest 1.78
CommonAspectRatio::TallFourByFive => Resolution::Portrait3x4, // 0.80, nearest 0.75
CommonAspectRatio::TallTwoByThree => Resolution::Portrait3x4, // 0.67, nearest 0.75
CommonAspectRatio::TallNineByTwentyOne => Resolution::Portrait9x16, // 0.43, nearest 0.56
_ => Resolution::Square1x1,
}
}
// Seedance2p0 supports batch counts of 1, 2, and 4 only.
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<BatchCount, ArtcraftRouterError> {
let count = video_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(BatchCount::One),
2 => Ok(BatchCount::Two),
4 => Ok(BatchCount::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "video_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
Ok(if count < 4 { BatchCount::Four } else { BatchCount::Four })
}
RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(if count < 4 { BatchCount::Two } else { BatchCount::Four })
}
},
}
}
// Seedance2p0 supports duration of 4–15 seconds.
fn plan_duration(
duration_seconds: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<u8, ArtcraftRouterError> {
const MIN: u16 = 4;
const MAX: u16 = 15;
const DEFAULT: u8 = 5;
match duration_seconds {
None => Ok(DEFAULT),
Some(d) if d >= MIN && d <= MAX => Ok(d as u8),
Some(d) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "duration_seconds",
value: format!("{}", d),
}))
}
_ => Ok(d.clamp(MIN, MAX) as u8),
},
}
}
@@ -4,15 +4,21 @@ use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraf
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_kling3p0_standard::estimate_video_cost_artcraft_kling3p0_standard;
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance1p5_pro::estimate_video_cost_artcraft_seedance1p5_pro;
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance2p0::estimate_video_cost_artcraft_seedance2p0;
use crate::generate::generate_video::cost::muapi::estimate_video_cost_muapi_seedance2p0::estimate_video_cost_muapi_seedance2p0;
use crate::generate::generate_video::cost::seedance2pro::estimate_video_cost_seedance2pro_seedance2p0::estimate_video_cost_seedance2pro_seedance2p0;
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling3p0_pro::execute_artcraft_kling3p0_pro;
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling3p0_standard::execute_artcraft_kling3p0_standard;
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_seedance1p5_pro::execute_artcraft_seedance1p5_pro;
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_seedance2p0::execute_artcraft_seedance2p0;
use crate::generate::generate_video::execute::muapi::execute_muapi_seedance2p0::execute_muapi_seedance2p0;
use crate::generate::generate_video::execute::seedance2pro::execute_seedance2pro_seedance2p0::execute_seedance2pro_seedance2p0;
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_pro::PlanArtcraftKling3p0Pro;
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::PlanArtcraftKling3p0Standard;
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance1p5_pro::PlanArtcraftSeedance1p5Pro;
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
use crate::generate::generate_video::plan::muapi::plan_generate_video_muapi_seedance2p0::PlanMuapiSeedance2p0;
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::PlanSeedance2proSeedance2p0;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
#[derive(Debug)]
@@ -21,6 +27,8 @@ pub enum VideoGenerationPlan<'a> {
ArtcraftKling3p0Standard(PlanArtcraftKling3p0Standard<'a>),
ArtcraftSeedance1p5Pro(PlanArtcraftSeedance1p5Pro<'a>),
ArtcraftSeedance2p0(PlanArtcraftSeedance2p0<'a>),
MuapiSeedance2p0(PlanMuapiSeedance2p0),
Seedance2proSeedance2p0(PlanSeedance2proSeedance2p0),
}
impl<'a> VideoGenerationPlan<'a> {
@@ -45,6 +53,14 @@ impl<'a> VideoGenerationPlan<'a> {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_seedance2p0(plan, artcraft_client).await
}
VideoGenerationPlan::MuapiSeedance2p0(plan) => {
let muapi_client = client.get_muapi_client_ref()?;
execute_muapi_seedance2p0(plan, muapi_client).await
}
VideoGenerationPlan::Seedance2proSeedance2p0(plan) => {
let seedance2pro_client = client.get_seedance2pro_client_ref()?;
execute_seedance2pro_seedance2p0(plan, seedance2pro_client).await
}
}
}
@@ -62,6 +78,12 @@ impl<'a> VideoGenerationPlan<'a> {
VideoGenerationPlan::ArtcraftSeedance2p0(plan) => {
estimate_video_cost_artcraft_seedance2p0(plan)
}
VideoGenerationPlan::MuapiSeedance2p0(plan) => {
estimate_video_cost_muapi_seedance2p0(plan)
}
VideoGenerationPlan::Seedance2proSeedance2p0(plan) => {
estimate_video_cost_seedance2pro_seedance2p0(plan)
}
}
}
}
@@ -2,6 +2,7 @@ pub mod api;
pub mod client;
pub mod errors;
pub mod generate;
pub mod utils;
#[cfg(test)]
pub mod test_helpers;
@@ -0,0 +1,30 @@
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::download_error::DownloadError;
/// Download a file from a URL, returning its bytes.
pub async fn download_file(url: &str) -> Result<Vec<u8>, ArtcraftRouterError> {
let response = reqwest::get(url)
.await
.map_err(|err| DownloadError::ReqwestDownload {
url: url.to_string(),
maybe_status: err.status(),
error: err,
})?;
let status = response.status();
if !status.is_success() {
return Err(DownloadError::BadStatus {
url: url.to_string(),
status_code: status.as_u16(),
}.into());
}
response.bytes()
.await
.map(|b| b.to_vec())
.map_err(|err| DownloadError::ReqwestDownload {
url: url.to_string(),
maybe_status: err.status(),
error: err,
}.into())
}
@@ -0,0 +1 @@
pub mod download_file;
@@ -93,6 +93,42 @@ pub struct Seedance2p0ImageToVideoArgs<'a> {
pub quality: Quality,
}
impl Seedance2p0ImageToVideoArgs<'_> {
/// Estimates the credit cost for this generation request.
///
/// Pricing (as of 2026-03):
///
/// | Duration | Basic | High |
/// |----------|-------|------|
/// | 5s | 0.60 | 1.25 |
/// | 10s | 1.20 | 2.50 |
/// | 15s | 1.80 | 3.75 |
pub fn estimate_credits(&self) -> f64 {
match (&self.quality, &self.duration) {
(Quality::Basic, Duration::FiveSeconds) => 0.6,
(Quality::Basic, Duration::TenSeconds) => 1.2,
(Quality::Basic, Duration::FifteenSeconds) => 1.8,
(Quality::High, Duration::FiveSeconds) => 1.25,
(Quality::High, Duration::TenSeconds) => 2.5,
(Quality::High, Duration::FifteenSeconds) => 3.75,
}
}
/// Estimates the USD-cent cost for this generation request.
///
/// Conversion: $1.00 = 1.0 credits.
///
/// | Duration | Basic | High |
/// |----------|-------|------|
/// | 5s | 60¢ | 125¢ |
/// | 10s | 120¢ | 250¢ |
/// | 15s | 180¢ | 375¢ |
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
let credits = self.estimate_credits();
(credits * 100.0).round() as u64
}
}
impl std::fmt::Debug for Seedance2p0ImageToVideoArgs<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Seedance2p0ImageToVideoArgs")
@@ -173,6 +209,49 @@ mod tests {
use log::LevelFilter;
use test_data::web::image_urls::*;
fn dummy_session() -> MuapiSession {
use crate::creds::muapi_api_key::MuapiApiKey;
MuapiSession::new(MuapiApiKey::new(String::new()))
}
fn args_with(duration: Duration, quality: Quality) -> Seedance2p0ImageToVideoArgs<'static> {
let session = Box::leak(Box::new(dummy_session()));
Seedance2p0ImageToVideoArgs {
session,
prompt: String::new(),
image_urls: vec![],
aspect_ratio: AspectRatio::Landscape16x9,
duration,
quality,
}
}
#[test]
fn test_estimate_credits() {
// Basic
assert_eq!(args_with(Duration::FiveSeconds, Quality::Basic).estimate_credits(), 0.6);
assert_eq!(args_with(Duration::TenSeconds, Quality::Basic).estimate_credits(), 1.2);
assert_eq!(args_with(Duration::FifteenSeconds, Quality::Basic).estimate_credits(), 1.8);
// High
assert_eq!(args_with(Duration::FiveSeconds, Quality::High).estimate_credits(), 1.25);
assert_eq!(args_with(Duration::TenSeconds, Quality::High).estimate_credits(), 2.5);
assert_eq!(args_with(Duration::FifteenSeconds, Quality::High).estimate_credits(), 3.75);
}
#[test]
fn test_estimate_cost_in_usd_cents() {
// Basic
assert_eq!(args_with(Duration::FiveSeconds, Quality::Basic).estimate_cost_in_usd_cents(), 60);
assert_eq!(args_with(Duration::TenSeconds, Quality::Basic).estimate_cost_in_usd_cents(), 120);
assert_eq!(args_with(Duration::FifteenSeconds, Quality::Basic).estimate_cost_in_usd_cents(), 180);
// High
assert_eq!(args_with(Duration::FiveSeconds, Quality::High).estimate_cost_in_usd_cents(), 125);
assert_eq!(args_with(Duration::TenSeconds, Quality::High).estimate_cost_in_usd_cents(), 250);
assert_eq!(args_with(Duration::FifteenSeconds, Quality::High).estimate_cost_in_usd_cents(), 375);
}
#[tokio::test]
#[ignore] // manually test — requires real API key
async fn test_seedance_2p0_image_to_video() -> AnyhowResult<()> {
@@ -90,6 +90,42 @@ pub struct Seedance2p0TextToVideoArgs<'a> {
pub quality: Quality,
}
impl Seedance2p0TextToVideoArgs<'_> {
/// Estimates the credit cost for this generation request.
///
/// Pricing (as of 2026-03):
///
/// | Duration | Basic | High |
/// |----------|-------|------|
/// | 5s | 0.60 | 1.25 |
/// | 10s | 1.20 | 2.50 |
/// | 15s | 1.80 | 3.75 |
pub fn estimate_credits(&self) -> f64 {
match (&self.quality, &self.duration) {
(Quality::Basic, Duration::FiveSeconds) => 0.6,
(Quality::Basic, Duration::TenSeconds) => 1.2,
(Quality::Basic, Duration::FifteenSeconds) => 1.8,
(Quality::High, Duration::FiveSeconds) => 1.25,
(Quality::High, Duration::TenSeconds) => 2.5,
(Quality::High, Duration::FifteenSeconds) => 3.75,
}
}
/// Estimates the USD-cent cost for this generation request.
///
/// Conversion: $1.00 = 1.0 credits.
///
/// | Duration | Basic | High |
/// |----------|-------|------|
/// | 5s | 60¢ | 125¢ |
/// | 10s | 120¢ | 250¢ |
/// | 15s | 180¢ | 375¢ |
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
let credits = self.estimate_credits();
(credits * 100.0).round() as u64
}
}
impl std::fmt::Debug for Seedance2p0TextToVideoArgs<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Seedance2p0TextToVideoArgs")
@@ -162,11 +198,53 @@ pub async fn seedance_2p0_text_to_video(
#[cfg(test)]
mod tests {
use super::*;
use crate::creds::muapi_api_key::MuapiApiKey;
use crate::test_utils::get_test_api_key::get_test_api_key;
use crate::test_utils::setup_test_logging::setup_test_logging;
use errors::AnyhowResult;
use log::LevelFilter;
fn dummy_session() -> MuapiSession {
MuapiSession::new(MuapiApiKey::new(String::new()))
}
fn args_with(duration: Duration, quality: Quality) -> Seedance2p0TextToVideoArgs<'static> {
let session = Box::leak(Box::new(dummy_session()));
Seedance2p0TextToVideoArgs {
session,
prompt: String::new(),
aspect_ratio: AspectRatio::Landscape16x9,
duration,
quality,
}
}
#[test]
fn test_estimate_credits() {
// Basic
assert_eq!(args_with(Duration::FiveSeconds, Quality::Basic).estimate_credits(), 0.6);
assert_eq!(args_with(Duration::TenSeconds, Quality::Basic).estimate_credits(), 1.2);
assert_eq!(args_with(Duration::FifteenSeconds, Quality::Basic).estimate_credits(), 1.8);
// High
assert_eq!(args_with(Duration::FiveSeconds, Quality::High).estimate_credits(), 1.25);
assert_eq!(args_with(Duration::TenSeconds, Quality::High).estimate_credits(), 2.5);
assert_eq!(args_with(Duration::FifteenSeconds, Quality::High).estimate_credits(), 3.75);
}
#[test]
fn test_estimate_cost_in_usd_cents() {
// Basic
assert_eq!(args_with(Duration::FiveSeconds, Quality::Basic).estimate_cost_in_usd_cents(), 60);
assert_eq!(args_with(Duration::TenSeconds, Quality::Basic).estimate_cost_in_usd_cents(), 120);
assert_eq!(args_with(Duration::FifteenSeconds, Quality::Basic).estimate_cost_in_usd_cents(), 180);
// High
assert_eq!(args_with(Duration::FiveSeconds, Quality::High).estimate_cost_in_usd_cents(), 125);
assert_eq!(args_with(Duration::TenSeconds, Quality::High).estimate_cost_in_usd_cents(), 250);
assert_eq!(args_with(Duration::FifteenSeconds, Quality::High).estimate_cost_in_usd_cents(), 375);
}
#[tokio::test]
#[ignore] // manually test — requires real API key
async fn test_seedance_2p0_text_to_video_landscape() -> AnyhowResult<()> {
@@ -228,4 +228,17 @@ mod tests {
let ext = extract_extension_from_url_str(url, &ExtractExtensions::All);
assert!(ext.is_none());
}
fn extract_extension_all(url: &str) -> Option<String> {
extract_extension_from_url_str(url, &ExtractExtensions::All)
.map(|ext| ext.without_period().to_string())
}
#[test]
fn test_extract_various_extensions() {
assert_eq!(extract_extension_all("https://example.com/image.png").as_deref(), Some("png"));
assert_eq!(extract_extension_all("https://example.com/video.mp4?token=abc").as_deref(), Some("mp4"));
assert_eq!(extract_extension_all("https://example.com/path/to/file.jpg").as_deref(), Some("jpg"));
assert_eq!(extract_extension_all("https://example.com/noext"), None);
}
}