diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/estimate_video_cost_seedance2pro_seedance2p0_fast.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/estimate_video_cost_seedance2pro_seedance2p0_fast.rs new file mode 100644 index 0000000000..dc19144d43 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/estimate_video_cost_seedance2pro_seedance2p0_fast.rs @@ -0,0 +1,129 @@ +use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession; +use seedance2pro_client::requests::generate_video::generate_video::{GenerateVideoArgs, ModelType, 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_fast( + 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, + model_type: ModelType::Seedance2Fast, // <-- Fast, not Pro + 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, + character_ids: None, + use_face_blur_hack: None, + host_override: 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, + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_video_model::CommonVideoModel; + use crate::api::provider::Provider; + use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; + use crate::generate::generate_video::generate_video_request::GenerateVideoRequest; + + fn estimate_usd_cents(duration_seconds: u16, video_batch_count: u16) -> u64 { + let request = GenerateVideoRequest { + model: CommonVideoModel::Seedance2p0Fast, + provider: Provider::Seedance2Pro, + prompt: None, + negative_prompt: None, + start_frame: None, + end_frame: None, + reference_images: None, + reference_videos: None, + reference_audio: None, + reference_character_tokens: None, + resolution: None, + aspect_ratio: None, + duration_seconds: Some(duration_seconds), + video_batch_count: Some(video_batch_count), + generate_audio: None, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, + idempotency_token: None, + }; + request.build() + .expect("build should succeed") + .estimate_costs() + .cost_in_usd_cents + .expect("cost_in_usd_cents should be present") + } + + #[test] + fn cost_is_present() { + let cents = estimate_usd_cents(5, 1); + assert!(cents > 0); + } + + #[test] + fn cost_batch_1() { + // Seedance2Fast: 28 credits/sec, 220 credits/$1 (with rounding) + assert_eq!(estimate_usd_cents(4, 1), 51); + assert_eq!(estimate_usd_cents(5, 1), 64); + assert_eq!(estimate_usd_cents(10, 1), 127); + assert_eq!(estimate_usd_cents(15, 1), 191); + } + + #[test] + fn cost_batch_2() { + assert_eq!(estimate_usd_cents(5, 2), 127); + } + + #[test] + fn cost_batch_4() { + assert_eq!(estimate_usd_cents(5, 4), 255); + } + + #[test] + fn fast_is_cheaper_than_pro() { + let fast = estimate_usd_cents(5, 1); + + let pro_request = GenerateVideoRequest { + model: CommonVideoModel::Seedance2p0, + provider: Provider::Seedance2Pro, + prompt: None, + negative_prompt: None, + start_frame: None, + end_frame: None, + reference_images: None, + reference_videos: None, + reference_audio: None, + reference_character_tokens: None, + resolution: None, + aspect_ratio: None, + duration_seconds: Some(5), + video_batch_count: Some(1), + generate_audio: None, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, + idempotency_token: None, + }; + let pro = pro_request.build().unwrap().estimate_costs().cost_in_usd_cents.unwrap(); + + assert!(fast < pro, "Fast ({}) should be cheaper than Pro ({})", fast, pro); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/mod.rs index d29759b40c..7bdb334f14 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/mod.rs @@ -1 +1,2 @@ pub mod estimate_video_cost_seedance2pro_seedance2p0; +pub mod estimate_video_cost_seedance2pro_seedance2p0_fast; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0.rs index fe72bfe644..43702873ee 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0.rs @@ -60,7 +60,7 @@ pub async fn execute_seedance2pro_seedance2p0( } /// Downloads a file from a source URL and re-uploads it to seedance2pro CDN. -async fn upload_to_seedance2pro( +pub(crate) async fn upload_to_seedance2pro( session: &seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession, source_url: &str, ) -> Result { @@ -89,7 +89,7 @@ async fn upload_to_seedance2pro( Ok(upload_response.public_url) } -async fn upload_optional_url( +pub(crate) async fn upload_optional_url( session: &seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession, url: Option<&str>, ) -> Result, ArtcraftRouterError> { @@ -99,7 +99,7 @@ async fn upload_optional_url( } } -async fn upload_optional_url_list( +pub(crate) async fn upload_optional_url_list( session: &seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession, urls: Option<&[String]>, ) -> Result>, ArtcraftRouterError> { diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0_fast.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0_fast.rs new file mode 100644 index 0000000000..8ff46cca6a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0_fast.rs @@ -0,0 +1,57 @@ +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::generate::generate_video::execute::seedance2pro::execute_seedance2pro_seedance2p0::{ + upload_optional_url, upload_optional_url_list, +}; +use seedance2pro_client::requests::generate_video::generate_video::{ + generate_video, GenerateVideoArgs, ModelType, +}; + +/// Execute Seedance 2.0 Fast via the Seedance2Pro/Kinovi provider. +/// +/// Identical to the Seedance 2.0 Pro executor except it uses +/// `ModelType::Seedance2Fast` instead of `ModelType::Seedance2Pro`. +pub async fn execute_seedance2pro_seedance2p0_fast( + plan: &PlanSeedance2proSeedance2p0, + seedance2pro_client: &RouterSeedance2ProClient, +) -> Result { + 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, + model_type: ModelType::Seedance2Fast, // <-- Fast, not Pro + 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, + character_ids: None, + use_face_blur_hack: None, + host_override: 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, + })) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/mod.rs index 98193dacca..35e435c85d 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/mod.rs @@ -1 +1,2 @@ pub mod execute_seedance2pro_seedance2p0; +pub mod execute_seedance2pro_seedance2p0_fast; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request.rs index d4f466aa1f..1f927552d3 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request.rs @@ -44,6 +44,7 @@ use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1: use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1_fast::plan_generate_video_fal_veo_3p1_fast; 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::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0_fast::plan_generate_video_seedance2pro_seedance2p0_fast; 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. @@ -174,6 +175,7 @@ impl<'a> GenerateVideoRequest<'a> { fn build_seedance2pro(&self) -> Result, ArtcraftRouterError> { match self.model { CommonVideoModel::Seedance2p0 => plan_generate_video_seedance2pro_seedance2p0(self), + CommonVideoModel::Seedance2p0Fast => plan_generate_video_seedance2pro_seedance2p0_fast(self), _ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))), } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/seedance2pro/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/seedance2pro/mod.rs index 19d854ae8a..3413f094c5 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/seedance2pro/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/seedance2pro/mod.rs @@ -1 +1,2 @@ pub mod plan_generate_video_seedance2pro_seedance2p0; +pub mod plan_generate_video_seedance2pro_seedance2p0_fast; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/seedance2pro/plan_generate_video_seedance2pro_seedance2p0_fast.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/seedance2pro/plan_generate_video_seedance2pro_seedance2p0_fast.rs new file mode 100644 index 0000000000..8b4ba233cb --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/seedance2pro/plan_generate_video_seedance2pro_seedance2p0_fast.rs @@ -0,0 +1,27 @@ +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::generate::generate_video::generate_video_request::GenerateVideoRequest; +use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::{ + PlanSeedance2proSeedance2p0, plan_generate_video_seedance2pro_seedance2p0, +}; +use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan; + +/// Plan for Seedance 2.0 Fast via Seedance2Pro provider. +/// +/// Uses the same plan structure as Seedance 2.0 Pro (same resolution, duration, +/// batch count, and media reference handling). The only difference is the model +/// type used at execution time. +pub fn plan_generate_video_seedance2pro_seedance2p0_fast<'a>( + request: &'a GenerateVideoRequest<'a>, +) -> Result, ArtcraftRouterError> { + // Reuse the Pro plan builder — it produces a PlanSeedance2proSeedance2p0. + // We wrap it in the Fast variant so execution dispatches to the Fast executor. + let pro_plan = plan_generate_video_seedance2pro_seedance2p0(request)?; + + // Extract the inner plan struct and re-wrap it. + match pro_plan { + VideoGenerationPlan::Seedance2proSeedance2p0(plan) => { + Ok(VideoGenerationPlan::Seedance2proSeedance2p0Fast(plan)) + } + _ => unreachable!("plan_generate_video_seedance2pro_seedance2p0 always returns Seedance2proSeedance2p0"), + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_plan.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_plan.rs index 6df6a39bd7..de132b2c28 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_plan.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_plan.rs @@ -35,6 +35,7 @@ use crate::generate::generate_video::cost::fal::estimate_video_cost_fal_veo_3p1: use crate::generate::generate_video::cost::fal::estimate_video_cost_fal_veo_3p1_fast::estimate_video_cost_fal_veo_3p1_fast; 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::cost::seedance2pro::estimate_video_cost_seedance2pro_seedance2p0_fast::estimate_video_cost_seedance2pro_seedance2p0_fast; use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling_1_6_pro::execute_artcraft_kling_1_6_pro; use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling_2_1_master::execute_artcraft_kling_2_1_master; use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling_2_1_pro::execute_artcraft_kling_2_1_pro; @@ -70,6 +71,7 @@ use crate::generate::generate_video::execute::fal::generate_video_fal_veo_3p1::e use crate::generate::generate_video::execute::fal::generate_video_fal_veo_3p1_fast::execute_fal_veo_3p1_fast; 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::execute::seedance2pro::execute_seedance2pro_seedance2p0_fast::execute_seedance2pro_seedance2p0_fast; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_1_6_pro::PlanArtcraftKling16Pro; use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_2_1_master::PlanArtcraftKling21Master; @@ -129,6 +131,7 @@ pub enum VideoGenerationPlan<'a> { ArtcraftVeo3p1Fast(PlanArtcraftVeo3p1Fast<'a>), MuapiSeedance2p0(PlanMuapiSeedance2p0), Seedance2proSeedance2p0(PlanSeedance2proSeedance2p0), + Seedance2proSeedance2p0Fast(PlanSeedance2proSeedance2p0), FalVeo2(PlanFalVeo2), FalVeo3(PlanFalVeo3), FalVeo3Fast(PlanFalVeo3Fast), @@ -229,6 +232,10 @@ impl<'a> VideoGenerationPlan<'a> { let seedance2pro_client = client.get_seedance2pro_client_ref()?; execute_seedance2pro_seedance2p0(plan, seedance2pro_client).await } + VideoGenerationPlan::Seedance2proSeedance2p0Fast(plan) => { + let seedance2pro_client = client.get_seedance2pro_client_ref()?; + execute_seedance2pro_seedance2p0_fast(plan, seedance2pro_client).await + } VideoGenerationPlan::FalVeo2(plan) => { let fal_client = client.get_fal_client_ref()?; execute_fal_veo_2(plan, fal_client).await @@ -335,6 +342,9 @@ impl<'a> VideoGenerationPlan<'a> { VideoGenerationPlan::Seedance2proSeedance2p0(plan) => { estimate_video_cost_seedance2pro_seedance2p0(plan) } + VideoGenerationPlan::Seedance2proSeedance2p0Fast(plan) => { + estimate_video_cost_seedance2pro_seedance2p0_fast(plan) + } VideoGenerationPlan::FalVeo2(plan) => estimate_video_cost_fal_veo_2(plan), VideoGenerationPlan::FalVeo3(plan) => estimate_video_cost_fal_veo_3(plan), VideoGenerationPlan::FalVeo3Fast(plan) => estimate_video_cost_fal_veo_3_fast(plan), diff --git a/crates/api_clients/worldlabs_api_client/test_data/images/safe_blue_square.png b/crates/api_clients/worldlabs_api_client/test_data/images/safe_blue_square.png new file mode 100644 index 0000000000..e06ac19715 Binary files /dev/null and b/crates/api_clients/worldlabs_api_client/test_data/images/safe_blue_square.png differ diff --git a/crates/api_clients/worldlabs_api_client/test_data/images/safe_gradient_512.png b/crates/api_clients/worldlabs_api_client/test_data/images/safe_gradient_512.png new file mode 100644 index 0000000000..66917cdeb3 Binary files /dev/null and b/crates/api_clients/worldlabs_api_client/test_data/images/safe_gradient_512.png differ diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs index f859303072..e61f532f42 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs @@ -159,6 +159,7 @@ pub async fn omni_gen_video_generate_handler( let execution_provider = match request.model { Some(CommonVideoModel::Seedance2p0) => Provider::Seedance2Pro, + Some(CommonVideoModel::Seedance2p0Fast) => Provider::Seedance2Pro, _ => Provider::Fal, };