diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs index c5585af9a3..7827fa37b4 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs @@ -62,6 +62,8 @@ use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::build:: use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::build::build_gmicloud_seedance_2p0_u; use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::build::build_gmicloud_seedance_2p0_u_fast; use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video::build::build_grok_api_grok_imagine_video; +use crate::generate::generate_video_v2::providers::fal::seedance_1p0_lite::build::build_fal_seedance_1p0_lite; +use crate::generate::generate_video_v2::providers::fal::seedance_1p5_pro::build::build_fal_seedance_1p5_pro; use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; @@ -165,6 +167,9 @@ impl GenerateVideoRequestBuilder { (Provider::Artcraft, CommonVideoModel::Seedance2p0BytePlusFast) => true, (Provider::Artcraft, CommonVideoModel::PreviewModel) => true, (Provider::Artcraft, CommonVideoModel::PreviewModelFast) => true, + // Fal + (Provider::Fal, CommonVideoModel::Seedance10Lite) => true, + (Provider::Fal, CommonVideoModel::Seedance1p5Pro) => true, // GmiCloud (Provider::GmiCloud, CommonVideoModel::Seedance2p0Ultra) => true, (Provider::GmiCloud, CommonVideoModel::Seedance2p0UltraFast) => true, @@ -192,6 +197,9 @@ impl GenerateVideoRequestBuilder { (Provider::Artcraft, CommonVideoModel::Seedance2p0BytePlusFast) => build_artcraft_seedance_2p0_bp_fast(self), (Provider::Artcraft, CommonVideoModel::PreviewModel) => build_artcraft_preview_model(self), (Provider::Artcraft, CommonVideoModel::PreviewModelFast) => build_artcraft_preview_model_fast(self), + // Fal + (Provider::Fal, CommonVideoModel::Seedance10Lite) => build_fal_seedance_1p0_lite(self), + (Provider::Fal, CommonVideoModel::Seedance1p5Pro) => build_fal_seedance_1p5_pro(self), // GmiCloud (Provider::GmiCloud, CommonVideoModel::Seedance2p0Ultra) => build_gmicloud_seedance_2p0_u(self), (Provider::GmiCloud, CommonVideoModel::Seedance2p0UltraFast) => build_gmicloud_seedance_2p0_u_fast(self), diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/cost_parity_tests.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/cost_parity_tests.rs new file mode 100644 index 0000000000..9a0c11f466 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/cost_parity_tests.rs @@ -0,0 +1,288 @@ +//! Combinatorial cost-parity tests between v1 (`build().estimate_cost()`) and v2 +//! (`build2().estimate_cost()`) for the Fal-hosted Seedance video models. +//! +//! The goal: every request that produces a price in v1 must produce the +//! identical price in v2. If any callsite picks a different price after the +//! port, billing diverges. These tests close that gap. +//! +//! Both pipelines ultimately delegate to the same `FalRequestCostCalculator` +//! impls in `fal_client`, so parity should hold by construction — but we +//! exercise a wide variety of inputs anyway to catch any drift in the request +//! plumbing (e.g. resolution/duration mapping errors, default-value mismatches, +//! aspect-ratio fallthroughs). + +#![cfg(test)] + +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::common_resolution::CommonResolution; +use crate::api::common_video_model::CommonVideoModel; +use crate::api::image_ref::ImageRef; +use crate::api::provider::Provider; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + +const DUMMY_IMAGE: &str = "https://example.com/dummy.png"; +const DUMMY_END_FRAME: &str = "https://example.com/dummy_end.png"; + +// ── seedance 1.0 lite ── + +mod seedance_1p0_lite { + use super::*; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance10Lite, + provider: Provider::Fal, + // 1.0 lite is image-to-video only — a start_frame is required. + start_frame: Some(ImageRef::Url(DUMMY_IMAGE.to_string())), + ..Default::default() + } + } + + fn all_resolutions() -> &'static [Option] { + &[ + None, + Some(CommonResolution::FourEightyP), + Some(CommonResolution::SevenTwentyP), + Some(CommonResolution::TenEightyP), + ] + } + + fn all_durations() -> &'static [Option] { + &[None, Some(5), Some(10)] + } + + fn supported_aspect_ratios() -> &'static [Option] { + &[ + None, + Some(CommonAspectRatio::Auto), + Some(CommonAspectRatio::Square), + Some(CommonAspectRatio::WideFourByThree), + Some(CommonAspectRatio::WideSixteenByNine), + Some(CommonAspectRatio::Wide), + Some(CommonAspectRatio::WideTwentyOneByNine), + Some(CommonAspectRatio::TallThreeByFour), + Some(CommonAspectRatio::TallNineBySixteen), + Some(CommonAspectRatio::Tall), + ] + } + + #[test] + fn cost_parity_full_combinatorial() { + let mut combos_checked = 0; + let mut combos_with_end_frame = 0; + + for &resolution in all_resolutions() { + for &duration in all_durations() { + for &aspect_ratio in supported_aspect_ratios() { + for include_end_frame in [false, true] { + let mut builder = base_builder(); + builder.resolution = resolution; + builder.duration_seconds = duration; + builder.aspect_ratio = aspect_ratio; + if include_end_frame { + builder.end_frame = Some(ImageRef::Url(DUMMY_END_FRAME.to_string())); + combos_with_end_frame += 1; + } + + let v1 = v1_cost(&builder); + let v2 = v2_cost(builder.clone()); + assert_eq!( + v1, v2, + "1.0 lite cost mismatch: res={:?} dur={:?} ar={:?} end={} → v1={:?} v2={:?}", + resolution, duration, aspect_ratio, include_end_frame, v1, v2, + ); + combos_checked += 1; + } + } + } + } + + // Sanity: we should be checking a meaningful number of combinations. + assert!(combos_checked >= 240, "expected ≥240 combos, got {}", combos_checked); + assert!(combos_with_end_frame > 0, "expected at least one with end_frame"); + } +} + +// ── seedance 1.5 pro ── + +mod seedance_1p5_pro { + use super::*; + + fn t2v_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance1p5Pro, + provider: Provider::Fal, + // No start_frame → text-to-video mode. + ..Default::default() + } + } + + fn i2v_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance1p5Pro, + provider: Provider::Fal, + start_frame: Some(ImageRef::Url(DUMMY_IMAGE.to_string())), + ..Default::default() + } + } + + fn all_resolutions() -> &'static [Option] { + &[ + None, + Some(CommonResolution::FourEightyP), + Some(CommonResolution::SevenTwentyP), + Some(CommonResolution::TenEightyP), + ] + } + + fn all_durations() -> &'static [Option] { + &[None, Some(4), Some(5), Some(6), Some(7), Some(8), Some(9), Some(10), Some(11), Some(12)] + } + + fn supported_aspect_ratios() -> &'static [Option] { + &[ + None, + Some(CommonAspectRatio::Auto), + Some(CommonAspectRatio::Square), + Some(CommonAspectRatio::WideFourByThree), + Some(CommonAspectRatio::WideSixteenByNine), + Some(CommonAspectRatio::Wide), + Some(CommonAspectRatio::WideTwentyOneByNine), + Some(CommonAspectRatio::TallThreeByFour), + Some(CommonAspectRatio::TallNineBySixteen), + Some(CommonAspectRatio::Tall), + ] + } + + fn audio_options() -> &'static [Option] { + &[None, Some(true), Some(false)] + } + + #[test] + fn cost_parity_t2v_combinatorial() { + let mut combos_checked = 0; + for &resolution in all_resolutions() { + for &duration in all_durations() { + for &aspect_ratio in supported_aspect_ratios() { + for &generate_audio in audio_options() { + let mut builder = t2v_builder(); + builder.resolution = resolution; + builder.duration_seconds = duration; + builder.aspect_ratio = aspect_ratio; + builder.generate_audio = generate_audio; + + let v1 = v1_cost(&builder); + let v2 = v2_cost(builder.clone()); + assert_eq!( + v1, v2, + "1.5 pro t2v cost mismatch: res={:?} dur={:?} ar={:?} audio={:?} → v1={:?} v2={:?}", + resolution, duration, aspect_ratio, generate_audio, v1, v2, + ); + combos_checked += 1; + } + } + } + } + assert!(combos_checked >= 1000, "expected ≥1000 combos, got {}", combos_checked); + } + + #[test] + fn cost_parity_i2v_combinatorial() { + let mut combos_checked = 0; + let mut combos_with_end_frame = 0; + for &resolution in all_resolutions() { + for &duration in all_durations() { + for &aspect_ratio in supported_aspect_ratios() { + for &generate_audio in audio_options() { + for include_end_frame in [false, true] { + let mut builder = i2v_builder(); + builder.resolution = resolution; + builder.duration_seconds = duration; + builder.aspect_ratio = aspect_ratio; + builder.generate_audio = generate_audio; + if include_end_frame { + builder.end_frame = Some(ImageRef::Url(DUMMY_END_FRAME.to_string())); + combos_with_end_frame += 1; + } + + let v1 = v1_cost(&builder); + let v2 = v2_cost(builder.clone()); + assert_eq!( + v1, v2, + "1.5 pro i2v cost mismatch: res={:?} dur={:?} ar={:?} audio={:?} end={} → v1={:?} v2={:?}", + resolution, duration, aspect_ratio, generate_audio, include_end_frame, v1, v2, + ); + combos_checked += 1; + } + } + } + } + } + assert!(combos_checked >= 2000, "expected ≥2000 combos, got {}", combos_checked); + assert!(combos_with_end_frame > 0, "expected at least one with end_frame"); + } + + #[test] + fn cost_parity_with_pay_less_downgrade_strategy() { + // Sanity: even when the mismatch strategy downgrades unsupported + // resolutions/durations, v1 and v2 should pick the same fallback. + let oddities: &[(Option, Option)] = &[ + (Some(CommonResolution::OneK), None), + (Some(CommonResolution::TwoK), Some(5)), + (Some(CommonResolution::FourK), Some(12)), + (None, Some(13)), + ]; + for &(resolution, duration) in oddities { + let mut builder = t2v_builder(); + builder.resolution = resolution; + builder.duration_seconds = duration; + builder.request_mismatch_mitigation_strategy = + RequestMismatchMitigationStrategy::PayLessDowngrade; + let v1 = v1_cost(&builder); + let v2 = v2_cost(builder.clone()); + assert_eq!(v1, v2, "PayLessDowngrade mismatch at res={:?} dur={:?}", resolution, duration); + } + } + + #[test] + fn cost_parity_with_pay_more_upgrade_strategy() { + let oddities: &[(Option, Option)] = &[ + (Some(CommonResolution::OneK), None), + (Some(CommonResolution::TwoK), Some(5)), + (Some(CommonResolution::FourK), Some(13)), + (None, Some(20)), + ]; + for &(resolution, duration) in oddities { + let mut builder = t2v_builder(); + builder.resolution = resolution; + builder.duration_seconds = duration; + builder.request_mismatch_mitigation_strategy = + RequestMismatchMitigationStrategy::PayMoreUpgrade; + let v1 = v1_cost(&builder); + let v2 = v2_cost(builder.clone()); + assert_eq!(v1, v2, "PayMoreUpgrade mismatch at res={:?} dur={:?}", resolution, duration); + } + } +} + +// ── helpers ── + +/// Returns `Some(cost_in_usd_cents)` from the v1 pipeline. If v1 errors out +/// (e.g. for an unsupported resolution with `ErrorOut` strategy), returns `None` +/// so the parity test can compare apples to apples. +fn v1_cost(builder: &GenerateVideoRequestBuilder) -> Option { + builder + .build() + .ok() + .and_then(|plan| plan.estimate_costs().cost_in_usd_cents) +} + +/// Same as `v1_cost`, but for the v2 pipeline. +fn v2_cost(builder: GenerateVideoRequestBuilder) -> Option { + builder + .build2() + .ok() + .and_then(|dor| dor.estimate_cost().ok()) + .and_then(|estimate| estimate.cost_in_usd_cents) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/mod.rs new file mode 100644 index 0000000000..121f9709a0 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/mod.rs @@ -0,0 +1,5 @@ +pub mod seedance_1p0_lite; +pub mod seedance_1p5_pro; + +#[cfg(test)] +mod cost_parity_tests; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p0_lite/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p0_lite/build.rs new file mode 100644 index 0000000000..c786881ce8 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p0_lite/build.rs @@ -0,0 +1,155 @@ +use fal_client::requests::webhook::video::image::enqueue_seedance_1_lite_image_to_video_webhook::{ + Seedance1LiteAspectRatio, Seedance1LiteDuration, Seedance1LiteRequest, Seedance1LiteResolution, +}; + +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::common_resolution::CommonResolution; +use crate::api::image_ref::ImageRef; +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_builder::GenerateVideoRequestBuilder; +use crate::generate::generate_video_v2::providers::fal::seedance_1p0_lite::request::FalSeedance10LiteRequestState; +use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + +pub fn build_fal_seedance_1p0_lite( + mut builder: GenerateVideoRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + let image_url = require_url( + builder.start_frame.take(), + "start_frame", + "Seedance 1.0 Lite requires a starting frame", + )?; + let end_frame_image_url = optional_url(builder.end_frame.take())?; + let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?; + let resolution = plan_resolution(builder.resolution.take(), strategy)?; + let duration = plan_duration(builder.duration_seconds.take(), strategy)?; + let prompt = builder.prompt.take().unwrap_or_default(); + + let request = Seedance1LiteRequest { + image_url, + end_frame_image_url, + prompt, + duration, + resolution, + aspect_ratio, + camera_fixed: false, + seed: None, + }; + + let state = FalSeedance10LiteRequestState { request }; + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalSeedance10Lite(state))) +} + +// ── Plan helpers (copies of v1 logic; kept in sync intentionally) ── + +fn require_url( + start_frame: Option, + field: &'static str, + msg: &'static str, +) -> Result { + match start_frame { + Some(ImageRef::Url(url)) => Ok(url), + Some(ImageRef::MediaFileToken(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field, + value: "Fal only supports image URLs, not media file tokens".to_string(), + })) + } + None => Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field, + value: msg.to_string(), + })), + } +} + +fn optional_url(image_ref: Option) -> Result, ArtcraftRouterError> { + match image_ref { + None => Ok(None), + Some(ImageRef::Url(url)) => Ok(Some(url)), + Some(ImageRef::MediaFileToken(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "end_frame", + value: "Fal only supports image URLs, not media file tokens".to_string(), + })) + } + } +} + +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use Seedance1LiteAspectRatio as Ar; + match aspect_ratio { + None => Ok(None), + + Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(Some(Ar::Auto)), + + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(Ar::Square)), + Some(CommonAspectRatio::WideFourByThree) => Ok(Some(Ar::FourByThree)), + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Ar::SixteenByNine)), + Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(Ar::TwentyOneByNine)), + Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(Ar::ThreeByFour)), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Ar::NineBySixteen)), + + Some(unsupported) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + _ => Ok(Some(Ar::Auto)), + }, + } +} + +fn plan_resolution( + resolution: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + use Seedance1LiteResolution as R; + match resolution { + None => Ok(R::SevenTwentyP), + Some(CommonResolution::FourEightyP) => Ok(R::FourEightyP), + Some(CommonResolution::SevenTwentyP) => Ok(R::SevenTwentyP), + Some(CommonResolution::TenEightyP) => Ok(R::TenEightyP), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "resolution", + value: format!("{:?}", other), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(R::TenEightyP), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(R::FourEightyP), + }, + } +} + +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + match duration_seconds { + None => Ok(Seedance1LiteDuration::FiveSeconds), + Some(5) => Ok(Seedance1LiteDuration::FiveSeconds), + Some(10) => Ok(Seedance1LiteDuration::TenSeconds), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", other), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Seedance1LiteDuration::TenSeconds), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Seedance1LiteDuration::FiveSeconds), + }, + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p0_lite/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p0_lite/cost.rs new file mode 100644 index 0000000000..e3ce813347 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p0_lite/cost.rs @@ -0,0 +1,29 @@ +use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video_v2::providers::fal::seedance_1p0_lite::request::FalSeedance10LiteRequestState; + +pub struct FalSeedance10LiteCostState { + request: FalSeedance10LiteRequestState, +} + +impl FalSeedance10LiteCostState { + pub fn from_request(request: &FalSeedance10LiteRequestState) -> Self { + Self { request: request.clone() } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + // Mirrors v1 — delegate to the Fal client's cost calculator for billing parity. + let cost_in_usd_cents = self.request.request.calculate_cost_in_cents(); + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + failures_are_refunded: None, + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p0_lite/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p0_lite/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p0_lite/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p0_lite/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p0_lite/request.rs new file mode 100644 index 0000000000..35c79305fd --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p0_lite/request.rs @@ -0,0 +1,40 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use fal_client::requests::webhook::video::image::enqueue_seedance_1_lite_image_to_video_webhook::{ + enqueue_seedance_1_lite_image_to_video_webhook, Seedance1LiteArgs, Seedance1LiteRequest, +}; + +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; + +#[derive(Clone, Debug)] +pub struct FalSeedance10LiteRequestState { + pub request: Seedance1LiteRequest, +} + +impl FalSeedance10LiteRequestState { + pub async fn send(&self, client: &RouterFalClient) -> Result { + let outbound_request: Arc = Arc::new(self.request.clone()); + + let args = Seedance1LiteArgs { + request: self.request.clone(), + api_key: &client.api_key, + webhook_url: client.webhook_url.as_str(), + }; + + let webhook_response = enqueue_seedance_1_lite_image_to_video_webhook(args) + .await + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + maybe_outbound_request: Some(outbound_request), + })) + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p5_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p5_pro/build.rs new file mode 100644 index 0000000000..b128006d36 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p5_pro/build.rs @@ -0,0 +1,286 @@ +use fal_client::requests::webhook::video::image::enqueue_seedance_1p5_pro_image_to_video_webhook::{ + EnqueueSeedance1p5ProImageToVideoAspectRatio, EnqueueSeedance1p5ProImageToVideoDuration, + EnqueueSeedance1p5ProImageToVideoRequest, EnqueueSeedance1p5ProImageToVideoResolution, +}; +use fal_client::requests::webhook::video::text::enqueue_seedance_1p5_pro_text_to_video_webhook::{ + EnqueueSeedance1p5ProTextToVideoAspectRatio, EnqueueSeedance1p5ProTextToVideoDuration, + EnqueueSeedance1p5ProTextToVideoRequest, EnqueueSeedance1p5ProTextToVideoResolution, +}; + +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::common_resolution::CommonResolution; +use crate::api::image_ref::ImageRef; +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_builder::GenerateVideoRequestBuilder; +use crate::generate::generate_video_v2::providers::fal::seedance_1p5_pro::request::{ + FalSeedance1p5ProMode, FalSeedance1p5ProRequestState, +}; +use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + +// Resolution / duration / aspect-ratio variants used for planning. Mirrors the +// v1 plan types, but lives entirely inside this module so the v2 dir is self-contained. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum PlanResolution { + FourEightyP, + SevenTwentyP, + TenEightyP, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum PlanDuration { + Four, + Five, + Six, + Seven, + Eight, + Nine, + Ten, + Eleven, + Twelve, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum PlanAspectRatio { + Auto, + TwentyOneByNine, + SixteenByNine, + FourByThree, + Square, + ThreeByFour, + NineBySixteen, +} + +pub fn build_fal_seedance_1p5_pro( + mut builder: GenerateVideoRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + // Decide t2v vs i2v based on whether a start_frame was provided. + let image_url = optional_url(builder.start_frame.take())?; + let end_image_url = optional_url(builder.end_frame.take())?; + + if image_url.is_none() && end_image_url.is_some() { + return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "end_frame", + value: "Seedance 1.5 Pro requires a start_frame when end_frame is provided".to_string(), + })); + } + + let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?; + let resolution = plan_resolution(builder.resolution.take(), strategy)?; + let duration = plan_duration(builder.duration_seconds.take(), strategy)?; + let prompt = builder.prompt.take().unwrap_or_default(); + let generate_audio = builder.generate_audio.take(); + + let mode = match image_url { + None => FalSeedance1p5ProMode::TextToVideo(EnqueueSeedance1p5ProTextToVideoRequest { + prompt, + resolution: resolution.map(to_t2v_resolution), + duration: duration.map(to_t2v_duration), + aspect_ratio: aspect_ratio.map(to_t2v_aspect_ratio), + generate_audio, + }), + Some(image_url) => FalSeedance1p5ProMode::ImageToVideo(EnqueueSeedance1p5ProImageToVideoRequest { + prompt, + image_url, + end_image_url, + resolution: resolution.map(to_i2v_resolution), + duration: duration.map(to_i2v_duration), + aspect_ratio: aspect_ratio.map(to_i2v_aspect_ratio), + generate_audio, + }), + }; + + let state = FalSeedance1p5ProRequestState { mode }; + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalSeedance1p5Pro(state))) +} + +// ── Plan helpers (kept in sync with v1 plan_generate_video_fal_seedance_1p5_pro.rs) ── + +fn optional_url(image_ref: Option) -> Result, ArtcraftRouterError> { + match image_ref { + None => Ok(None), + Some(ImageRef::Url(url)) => Ok(Some(url)), + Some(ImageRef::MediaFileToken(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "start_frame/end_frame", + value: "Fal only supports image URLs, not media file tokens".to_string(), + })) + } + } +} + +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use PlanAspectRatio as Ar; + match aspect_ratio { + None => Ok(None), + + Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(Some(Ar::Auto)), + + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(Ar::Square)), + Some(CommonAspectRatio::WideFourByThree) => Ok(Some(Ar::FourByThree)), + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Ar::SixteenByNine)), + Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(Ar::TwentyOneByNine)), + Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(Ar::ThreeByFour)), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Ar::NineBySixteen)), + + Some(unsupported) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + _ => Ok(Some(nearest_aspect_ratio(unsupported))), + }, + } +} + +/// Pick the nearest supported aspect ratio for unsupported inputs. +fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> PlanAspectRatio { + use PlanAspectRatio as Ar; + match aspect_ratio { + CommonAspectRatio::WideFiveByFour => Ar::FourByThree, + CommonAspectRatio::WideThreeByTwo => Ar::FourByThree, + CommonAspectRatio::TallFourByFive => Ar::ThreeByFour, + CommonAspectRatio::TallTwoByThree => Ar::ThreeByFour, + CommonAspectRatio::TallNineByTwentyOne => Ar::NineBySixteen, + _ => Ar::Square, + } +} + +fn plan_resolution( + resolution: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use PlanResolution as R; + match resolution { + None => Ok(None), + Some(CommonResolution::FourEightyP) => Ok(Some(R::FourEightyP)), + Some(CommonResolution::SevenTwentyP) => Ok(Some(R::SevenTwentyP)), + Some(CommonResolution::TenEightyP) => Ok(Some(R::TenEightyP)), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "resolution", + value: format!("{:?}", other), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(R::TenEightyP)), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(R::FourEightyP)), + }, + } +} + +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use PlanDuration as D; + match duration_seconds { + None => Ok(None), + Some(4) => Ok(Some(D::Four)), + Some(5) => Ok(Some(D::Five)), + Some(6) => Ok(Some(D::Six)), + Some(7) => Ok(Some(D::Seven)), + Some(8) => Ok(Some(D::Eight)), + Some(9) => Ok(Some(D::Nine)), + Some(10) => Ok(Some(D::Ten)), + Some(11) => Ok(Some(D::Eleven)), + Some(12) => Ok(Some(D::Twelve)), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", other), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(D::Twelve)), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(D::Four)), + }, + } +} + +// ── PlanXxx → fal_client request enum mapping ── + +fn to_t2v_resolution(r: PlanResolution) -> EnqueueSeedance1p5ProTextToVideoResolution { + use EnqueueSeedance1p5ProTextToVideoResolution as R; + match r { + PlanResolution::FourEightyP => R::FourEightyP, + PlanResolution::SevenTwentyP => R::SevenTwentyP, + PlanResolution::TenEightyP => R::TenEightyP, + } +} + +fn to_t2v_duration(d: PlanDuration) -> EnqueueSeedance1p5ProTextToVideoDuration { + use EnqueueSeedance1p5ProTextToVideoDuration as D; + match d { + PlanDuration::Four => D::FourSeconds, + PlanDuration::Five => D::FiveSeconds, + PlanDuration::Six => D::SixSeconds, + PlanDuration::Seven => D::SevenSeconds, + PlanDuration::Eight => D::EightSeconds, + PlanDuration::Nine => D::NineSeconds, + PlanDuration::Ten => D::TenSeconds, + PlanDuration::Eleven => D::ElevenSeconds, + PlanDuration::Twelve => D::TwelveSeconds, + } +} + +fn to_t2v_aspect_ratio(a: PlanAspectRatio) -> EnqueueSeedance1p5ProTextToVideoAspectRatio { + use EnqueueSeedance1p5ProTextToVideoAspectRatio as Ar; + match a { + PlanAspectRatio::Auto => Ar::Auto, + PlanAspectRatio::TwentyOneByNine => Ar::TwentyOneByNine, + PlanAspectRatio::SixteenByNine => Ar::SixteenByNine, + PlanAspectRatio::FourByThree => Ar::FourByThree, + PlanAspectRatio::Square => Ar::Square, + PlanAspectRatio::ThreeByFour => Ar::ThreeByFour, + PlanAspectRatio::NineBySixteen => Ar::NineBySixteen, + } +} + +fn to_i2v_resolution(r: PlanResolution) -> EnqueueSeedance1p5ProImageToVideoResolution { + use EnqueueSeedance1p5ProImageToVideoResolution as R; + match r { + PlanResolution::FourEightyP => R::FourEightyP, + PlanResolution::SevenTwentyP => R::SevenTwentyP, + PlanResolution::TenEightyP => R::TenEightyP, + } +} + +fn to_i2v_duration(d: PlanDuration) -> EnqueueSeedance1p5ProImageToVideoDuration { + use EnqueueSeedance1p5ProImageToVideoDuration as D; + match d { + PlanDuration::Four => D::FourSeconds, + PlanDuration::Five => D::FiveSeconds, + PlanDuration::Six => D::SixSeconds, + PlanDuration::Seven => D::SevenSeconds, + PlanDuration::Eight => D::EightSeconds, + PlanDuration::Nine => D::NineSeconds, + PlanDuration::Ten => D::TenSeconds, + PlanDuration::Eleven => D::ElevenSeconds, + PlanDuration::Twelve => D::TwelveSeconds, + } +} + +fn to_i2v_aspect_ratio(a: PlanAspectRatio) -> EnqueueSeedance1p5ProImageToVideoAspectRatio { + use EnqueueSeedance1p5ProImageToVideoAspectRatio as Ar; + match a { + PlanAspectRatio::Auto => Ar::Auto, + PlanAspectRatio::TwentyOneByNine => Ar::TwentyOneByNine, + PlanAspectRatio::SixteenByNine => Ar::SixteenByNine, + PlanAspectRatio::FourByThree => Ar::FourByThree, + PlanAspectRatio::Square => Ar::Square, + PlanAspectRatio::ThreeByFour => Ar::ThreeByFour, + PlanAspectRatio::NineBySixteen => Ar::NineBySixteen, + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p5_pro/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p5_pro/cost.rs new file mode 100644 index 0000000000..197307eb4e --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p5_pro/cost.rs @@ -0,0 +1,76 @@ +use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator; +use fal_client::requests::webhook::video::text::enqueue_seedance_1p5_pro_text_to_video_webhook::{ + EnqueueSeedance1p5ProTextToVideoDuration, EnqueueSeedance1p5ProTextToVideoRequest, + EnqueueSeedance1p5ProTextToVideoResolution, +}; +use fal_client::requests::webhook::video::image::enqueue_seedance_1p5_pro_image_to_video_webhook::{ + EnqueueSeedance1p5ProImageToVideoDuration, EnqueueSeedance1p5ProImageToVideoResolution, +}; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video_v2::providers::fal::seedance_1p5_pro::request::{ + FalSeedance1p5ProMode, FalSeedance1p5ProRequestState, +}; + +pub struct FalSeedance1p5ProCostState { + request: FalSeedance1p5ProRequestState, +} + +impl FalSeedance1p5ProCostState { + pub fn from_request(request: &FalSeedance1p5ProRequestState) -> Self { + Self { request: request.clone() } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + // Seedance 1.5 Pro t2v and i2v have identical pricing. v1 delegates both + // to the t2v calculator; v2 does the same to guarantee billing parity. + let t2v_request = match &self.request.mode { + FalSeedance1p5ProMode::TextToVideo(req) => req.clone(), + FalSeedance1p5ProMode::ImageToVideo(req) => EnqueueSeedance1p5ProTextToVideoRequest { + prompt: String::new(), + resolution: req.resolution.map(i2v_to_t2v_resolution), + duration: req.duration.map(i2v_to_t2v_duration), + aspect_ratio: None, + generate_audio: req.generate_audio, + }, + }; + + let cost_in_usd_cents = t2v_request.calculate_cost_in_cents(); + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + failures_are_refunded: None, + } + } +} + +fn i2v_to_t2v_resolution(r: EnqueueSeedance1p5ProImageToVideoResolution) -> EnqueueSeedance1p5ProTextToVideoResolution { + use EnqueueSeedance1p5ProImageToVideoResolution as I; + use EnqueueSeedance1p5ProTextToVideoResolution as T; + match r { + I::FourEightyP => T::FourEightyP, + I::SevenTwentyP => T::SevenTwentyP, + I::TenEightyP => T::TenEightyP, + } +} + +fn i2v_to_t2v_duration(d: EnqueueSeedance1p5ProImageToVideoDuration) -> EnqueueSeedance1p5ProTextToVideoDuration { + use EnqueueSeedance1p5ProImageToVideoDuration as I; + use EnqueueSeedance1p5ProTextToVideoDuration as T; + match d { + I::FourSeconds => T::FourSeconds, + I::FiveSeconds => T::FiveSeconds, + I::SixSeconds => T::SixSeconds, + I::SevenSeconds => T::SevenSeconds, + I::EightSeconds => T::EightSeconds, + I::NineSeconds => T::NineSeconds, + I::TenSeconds => T::TenSeconds, + I::ElevenSeconds => T::ElevenSeconds, + I::TwelveSeconds => T::TwelveSeconds, + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p5_pro/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p5_pro/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p5_pro/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p5_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p5_pro/request.rs new file mode 100644 index 0000000000..bbd338ef0d --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/seedance_1p5_pro/request.rs @@ -0,0 +1,63 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use fal_client::requests::webhook::video::image::enqueue_seedance_1p5_pro_image_to_video_webhook::{ + enqueue_seedance_1p5_pro_image_to_video_webhook, EnqueueSeedance1p5ProImageToVideoArgs, + EnqueueSeedance1p5ProImageToVideoRequest, +}; +use fal_client::requests::webhook::video::text::enqueue_seedance_1p5_pro_text_to_video_webhook::{ + enqueue_seedance_1p5_pro_text_to_video_webhook, EnqueueSeedance1p5ProTextToVideoArgs, + EnqueueSeedance1p5ProTextToVideoRequest, +}; + +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; + +#[derive(Clone, Debug)] +pub enum FalSeedance1p5ProMode { + TextToVideo(EnqueueSeedance1p5ProTextToVideoRequest), + ImageToVideo(EnqueueSeedance1p5ProImageToVideoRequest), +} + +#[derive(Clone, Debug)] +pub struct FalSeedance1p5ProRequestState { + pub mode: FalSeedance1p5ProMode, +} + +impl FalSeedance1p5ProRequestState { + pub async fn send(&self, client: &RouterFalClient) -> Result { + let (webhook_response, outbound_request): (_, Arc) = match &self.mode { + FalSeedance1p5ProMode::TextToVideo(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueSeedance1p5ProTextToVideoArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_seedance_1p5_pro_text_to_video_webhook(args).await, outbound) + } + FalSeedance1p5ProMode::ImageToVideo(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueSeedance1p5ProImageToVideoArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_seedance_1p5_pro_image_to_video_webhook(args).await, outbound) + } + }; + + let webhook_response = webhook_response + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + maybe_outbound_request: Some(outbound_request), + })) + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/mod.rs index df80e57fd2..d06c6c2d02 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/mod.rs @@ -1,4 +1,5 @@ pub mod artcraft; +pub mod fal; pub mod gmicloud; pub mod grok_api; pub mod kinovi; \ No newline at end of file diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs index df65ee8588..89b32b2cba 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs @@ -23,6 +23,10 @@ use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp::co use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp::request::ArtcraftSeedance2p0BytePlusRequestState; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp_fast::cost::ArtcraftSeedance2p0BytePlusFastCostState; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp_fast::request::ArtcraftSeedance2p0BytePlusFastRequestState; +use crate::generate::generate_video_v2::providers::fal::seedance_1p0_lite::cost::FalSeedance10LiteCostState; +use crate::generate::generate_video_v2::providers::fal::seedance_1p0_lite::request::FalSeedance10LiteRequestState; +use crate::generate::generate_video_v2::providers::fal::seedance_1p5_pro::cost::FalSeedance1p5ProCostState; +use crate::generate::generate_video_v2::providers::fal::seedance_1p5_pro::request::FalSeedance1p5ProRequestState; use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::cost::GmiCloudSeedance2p0UltraCostState; use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0UltraRequestState; use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::cost::GmiCloudSeedance2p0UltraFastCostState; @@ -48,6 +52,8 @@ pub enum VideoGenerationRequest { ArtcraftSeedance2p0UltraFast(ArtcraftSeedance2p0UltraFastRequestState), ArtcraftSeedance2p0BytePlus(ArtcraftSeedance2p0BytePlusRequestState), ArtcraftSeedance2p0BytePlusFast(ArtcraftSeedance2p0BytePlusFastRequestState), + FalSeedance10Lite(FalSeedance10LiteRequestState), + FalSeedance1p5Pro(FalSeedance1p5ProRequestState), GmiCloudSeedance2p0Ultra(GmiCloudSeedance2p0UltraRequestState), GmiCloudSeedance2p0UltraFast(GmiCloudSeedance2p0UltraFastRequestState), GrokApiGrokImagineVideo(GrokApiGrokImagineVideoRequestState), @@ -70,6 +76,8 @@ impl VideoGenerationRequest { Self::ArtcraftSeedance2p0UltraFast(_) => Provider::Artcraft, Self::ArtcraftSeedance2p0BytePlus(_) => Provider::Artcraft, Self::ArtcraftSeedance2p0BytePlusFast(_) => Provider::Artcraft, + Self::FalSeedance10Lite(_) => Provider::Fal, + Self::FalSeedance1p5Pro(_) => Provider::Fal, Self::GmiCloudSeedance2p0Ultra(_) => Provider::GmiCloud, Self::GmiCloudSeedance2p0UltraFast(_) => Provider::GmiCloud, Self::GrokApiGrokImagineVideo(_) => Provider::GrokApi, @@ -92,6 +100,8 @@ impl VideoGenerationRequest { VideoGenerationRequest::ArtcraftSeedance2p0UltraFast(request) => Ok(ArtcraftSeedance2p0UltraFastCostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSeedance2p0BytePlus(request) => Ok(ArtcraftSeedance2p0BytePlusCostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSeedance2p0BytePlusFast(request) => Ok(ArtcraftSeedance2p0BytePlusFastCostState::from_request(request).estimate_cost()), + VideoGenerationRequest::FalSeedance10Lite(request) => Ok(FalSeedance10LiteCostState::from_request(request).estimate_cost()), + VideoGenerationRequest::FalSeedance1p5Pro(request) => Ok(FalSeedance1p5ProCostState::from_request(request).estimate_cost()), VideoGenerationRequest::GmiCloudSeedance2p0Ultra(request) => Ok(GmiCloudSeedance2p0UltraCostState::from_request(request).estimate_cost()), VideoGenerationRequest::GmiCloudSeedance2p0UltraFast(request) => Ok(GmiCloudSeedance2p0UltraFastCostState::from_request(request).estimate_cost()), VideoGenerationRequest::GrokApiGrokImagineVideo(request) => Ok(GrokApiGrokImagineVideoCostState::from_request(request).estimate_cost()), @@ -145,6 +155,14 @@ impl VideoGenerationRequest { let client_ref = client.get_artcraft_client_ref()?; request.send(client_ref).await }, + VideoGenerationRequest::FalSeedance10Lite(request) => { + let client_ref = client.get_fal_client_ref()?; + request.send(client_ref).await + }, + VideoGenerationRequest::FalSeedance1p5Pro(request) => { + let client_ref = client.get_fal_client_ref()?; + request.send(client_ref).await + }, VideoGenerationRequest::GmiCloudSeedance2p0Ultra(request) => { let client_ref = client.get_gmicloud_client_ref()?; request.send(client_ref).await