From 534337aa3a7e2fc3c67f6ebbcba918703d851ec8 Mon Sep 17 00:00:00 2001 From: hanashi Date: Sat, 8 Aug 2026 21:46:14 -0400 Subject: [PATCH] seedance 2.5 ultra (#1870) --- .../src/api/router_video_model.rs | 12 + .../generate_video_request_builder.rs | 2 + .../generate_video/providers/artcraft/mod.rs | 1 + .../artcraft/seedance_2p5_u/build.rs | 246 ++++++++++++++++++ .../providers/artcraft/seedance_2p5_u/cost.rs | 213 +++++++++++++++ .../providers/artcraft/seedance_2p5_u/mod.rs | 3 + .../artcraft/seedance_2p5_u/request.rs | 22 ++ .../providers/artcraft/seedance_common.rs | 42 +++ .../video_generation_request.rs | 9 + .../inference_model_type.rs | 10 + .../common/generation/common_model_type.rs | 13 +- .../common/generation/common_video_model.rs | 10 + .../by_type/seedance_2p5_video_models.rs | 41 +++ .../video/estimate_video_cost_handler.rs | 1 + .../video/omni_api_video_generate_handler.rs | 3 +- .../video/pipeline_v2/run_pipeline_v2.rs | 10 + .../cost/video/omni_gen_video_cost_handler.rs | 18 +- .../video/omni_gen_video_generate_handler.rs | 3 +- .../video/pipeline_v2/run_pipeline_v2.rs | 10 + 19 files changed, 665 insertions(+), 4 deletions(-) create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/request.rs diff --git a/crates/api_clients/artcraft/artcraft_router/src/api/router_video_model.rs b/crates/api_clients/artcraft/artcraft_router/src/api/router_video_model.rs index e87ca674b8..9b6b543708 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/api/router_video_model.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/api/router_video_model.rs @@ -95,6 +95,11 @@ pub enum RouterVideoModel { #[serde(rename = "seedance_2p5")] Seedance2p5, + /// Seedance 2.5 as routed via the BytePlus Ultra kinovi account. + /// Fulfilled by Seedance 2.5; billed at its own (higher) rate. + #[serde(rename = "seedance_2p5_u")] + Seedance2p5Ultra, + #[serde(rename = "happy_horse_1p0")] HappyHorse1p0, @@ -166,6 +171,13 @@ mod tests { assert_serde_round_trip(RouterVideoModel::Seedance2p5, "seedance_2p5"); } + #[test] + fn seedance_2p5_ultra_serializes() { + // NB: This string must match `CommonVideoModel` — the two enums convert + // via serde string round-trip in the omni handlers. + assert_serde_round_trip(RouterVideoModel::Seedance2p5Ultra, "seedance_2p5_u"); + } + #[test] fn veo_3p1_lite_and_vidu_variants_serialize() { // NB: These strings must match `CommonVideoModel` — the two enums convert 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 37ebb3a9f4..1a2d2c840f 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 @@ -79,6 +79,7 @@ use crate::generate::generate_video::providers::fal::vidu_q3_turbo::build::build use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast; use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::build::build_kinovi_seedance_2p0_mini; use crate::generate::generate_video::providers::artcraft::seedance_2p5::build::build_artcraft_seedance_2p5; +use crate::generate::generate_video::providers::artcraft::seedance_2p5_u::build::build_artcraft_seedance_2p5_u; use crate::generate::generate_video::providers::artcraft::seedance_2p5_preview::build::build_artcraft_seedance_2p5_preview; use crate::generate::generate_video::providers::kinovi::seedance_2p5::build::build_kinovi_seedance_2p5; use crate::generate::generate_video::providers::kinovi::seedance_2p5_preview::build::build_kinovi_seedance_2p5_preview; @@ -211,6 +212,7 @@ impl GenerateVideoRequestBuilder { (RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusUltraMini) => build_artcraft_seedance_2p0_bpu_mini(self), (RouterProvider::Artcraft, RouterVideoModel::Seedance2p5Preview) => build_artcraft_seedance_2p5_preview(self), (RouterProvider::Artcraft, RouterVideoModel::Seedance2p5) => build_artcraft_seedance_2p5(self), + (RouterProvider::Artcraft, RouterVideoModel::Seedance2p5Ultra) => build_artcraft_seedance_2p5_u(self), (RouterProvider::Artcraft, RouterVideoModel::PreviewModel) => build_artcraft_preview_model(self), (RouterProvider::Artcraft, RouterVideoModel::PreviewModelFast) => build_artcraft_preview_model_fast(self), (RouterProvider::Artcraft, RouterVideoModel::Seedance10Lite) => build_artcraft_seedance_1p0_lite(self), diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/mod.rs index 81f7548366..0044f19004 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/mod.rs @@ -34,6 +34,7 @@ pub mod seedance_2p0_bp_mini; pub mod seedance_2p0_bpu_mini; pub mod seedance_2p5; pub mod seedance_2p5_preview; +pub mod seedance_2p5_u; pub mod sora_2; pub mod sora_2_pro; pub mod veo_2; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/build.rs new file mode 100644 index 0000000000..f0b5865dd6 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/build.rs @@ -0,0 +1,246 @@ +use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + +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::providers::artcraft::build_common::{ + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, +}; +use crate::generate::generate_video::providers::artcraft::seedance_2p5_u::request::ArtcraftSeedance2p5UltraRequestState; +use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video::video_generation_request::VideoGenerationRequest; + +/// Seedance 2.5 Ultra: the same model surface as Seedance 2.5, billed at its +/// own (higher) rate and fulfilled via the BytePlus Ultra kinovi account. +pub fn build_artcraft_seedance_2p5_u(mut builder: GenerateVideoRequestBuilder) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + // Parity with the Kinovi build: keyframes and reference media are mutually + // exclusive, an end frame requires a start frame, and there is no + // character support. Erroring here keeps the billing estimate consistent + // with the execution build. + let has_keyframes = builder.start_frame.is_some() || builder.end_frame.is_some(); + let has_references = builder.reference_images.is_some() + || builder.reference_videos.is_some() + || builder.reference_audio.is_some(); + + if has_keyframes && has_references { + return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "reference_images", + value: "Seedance 2.5 Ultra cannot combine keyframes with reference media; send one or the other".to_string(), + })); + } + if builder.end_frame.is_some() && builder.start_frame.is_none() { + return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "end_frame", + value: "Seedance 2.5 Ultra requires a start frame when an end frame is set".to_string(), + })); + } + if builder.reference_character_tokens.is_some() { + return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "reference_character_tokens", + value: "Seedance 2.5 Ultra does not support character references".to_string(), + })); + } + + // 2.5 allows 4-30 second durations (longer than the shared 4-15 range in + // build_common) and generates a single video per request. Plan both here, + // leaving None for the shared builder, then set them after. + let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?; + plan_batch_count(builder.video_batch_count.take(), strategy)?; + + let total_input_seconds = builder.total_reference_video_input_seconds.take(); + + let mut request = build_artcraft_omni_video_request( + builder, + CommonVideoModelEnum::Seedance2p5Ultra, + SupportedResolutions::Fast, + UltraWideSupport::Supported, + )?; + request.duration_seconds = duration_seconds; + request.video_batch_count = Some(1); + + let state = ArtcraftSeedance2p5UltraRequestState { request, total_input_seconds }; + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5Ultra(state))) +} + +// Seedance 2.5 supports durations of 4-30 seconds. +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + const MIN: u16 = 4; + const MAX: u16 = 30; + match duration_seconds { + None => Ok(None), + Some(d) if d >= MIN && d <= MAX => Ok(Some(d)), + Some(d) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", d), + })) + } + _ => Ok(Some(d.clamp(MIN, MAX))), + }, + } +} + +// Seedance 2.5 generates a single video per request (no batching). +fn plan_batch_count( + video_batch_count: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result<(), ArtcraftRouterError> { + let count = video_batch_count.unwrap_or(1); + match count { + 0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)), + 1 => Ok(()), + _ => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "video_batch_count", + value: format!("{}", count), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade + | RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(()), + }, + } +} + +#[cfg(test)] +mod tests { + use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; + use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + use tokens::tokens::characters::CharacterToken; + use tokens::tokens::media_files::MediaFileToken; + + use crate::api::character_list_ref::CharacterListRef; + use crate::api::image_list_ref::ImageListRef; + use crate::api::image_ref::ImageRef; + use crate::api::router_provider::RouterProvider; + use crate::api::router_resolution::RouterResolution; + use crate::api::router_video_model::RouterVideoModel; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest; + use crate::generate::generate_video::video_generation_request::VideoGenerationRequest; + + use super::*; + + mod field_conversions { + use super::*; + + #[test] + fn model_is_correct() { + let req = unwrap_request(builder_with(|_| {})); + assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p5Ultra))); + } + + #[test] + fn duration_30_is_passed_through() { + let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(30); })); + assert_eq!(req.request.duration_seconds, Some(30)); + } + + #[test] + fn duration_clamped_to_range() { + let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(99); })); + assert_eq!(req.request.duration_seconds, Some(30)); + + let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(2); })); + assert_eq!(req.request.duration_seconds, Some(4)); + } + + #[test] + fn batch_count_is_always_one() { + let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(4); })); + assert_eq!(req.request.video_batch_count, Some(1)); + } + + #[test] + fn total_input_seconds_is_carried_through() { + let req = unwrap_request(builder_with(|b| { b.total_reference_video_input_seconds = Some(14); })); + assert_eq!(req.total_input_seconds, Some(14)); + } + } + + mod modality_rules { + use super::*; + + #[test] + fn keyframes_are_accepted() { + let req = unwrap_request(builder_with(|b| { + b.start_frame = Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start".to_string()))); + b.end_frame = Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_end".to_string()))); + })); + assert!(req.request.start_frame_image_media_token.is_some()); + assert!(req.request.end_frame_image_media_token.is_some()); + } + + #[test] + fn end_frame_without_start_frame_errors() { + let result = build_artcraft_seedance_2p5_u(builder_with(|b| { + b.end_frame = Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_end".to_string()))); + })); + assert!(result.is_err()); + } + + #[test] + fn keyframes_plus_references_error() { + let result = build_artcraft_seedance_2p5_u(builder_with(|b| { + b.start_frame = Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start".to_string()))); + b.reference_images = Some(ImageListRef::MediaFileTokens(vec![ + MediaFileToken::new("mf_ref".to_string()), + ])); + })); + assert!(result.is_err()); + } + + #[test] + fn character_references_error() { + let result = build_artcraft_seedance_2p5_u(builder_with(|b| { + b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(vec![ + CharacterToken::new("char_abc".to_string()), + ])); + })); + assert!(result.is_err()); + } + } + + mod resolution_tests { + use super::*; + + #[test] + fn res_480p() { + let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::FourEightyP); })); + assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP)); + } + + #[test] + fn res_1080p_downgrades_to_720p() { + let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::TenEightyP); })); + assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP)); + } + } + + fn builder_with(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder { + let mut builder = GenerateVideoRequestBuilder { + model: RouterVideoModel::Seedance2p5Ultra, + provider: RouterProvider::Artcraft, + duration_seconds: Some(5), + video_batch_count: Some(1), + ..Default::default() + }; + f(&mut builder); + builder + } + + fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p5UltraRequestState { + let result = build_artcraft_seedance_2p5_u(builder).expect("build should succeed"); + match result { + VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5Ultra(state)) => state, + _ => panic!("expected ArtcraftSeedance2p5Ultra request"), + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/cost.rs new file mode 100644 index 0000000000..318120b08b --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/cost.rs @@ -0,0 +1,213 @@ +use enums::common::generation::common_resolution::CommonResolution; +use kinovi_web_client::generate::video::generate_seedance_2p5::MAX_BILLED_INPUT_SECONDS; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p5_ultra_usd_cents; +use crate::generate::generate_video::providers::artcraft::seedance_2p5_u::request::ArtcraftSeedance2p5UltraRequestState; + +/// Seedance 2.5 Ultra pricing has the same structure as Seedance 2.5 (at +/// higher rates): it depends on the resolution, the output duration, and — +/// when reference videos are attached — the total seconds of reference video +/// input, which are billed on top of the output duration (at a lower +/// per-second rate). Input seconds are clamped to +/// [`MAX_BILLED_INPUT_SECONDS`] — the model accepts at most 30 seconds of +/// video. No batching. +pub struct ArtcraftSeedance2p5UltraCostState { + pub resolution: CommonResolution, + pub duration_seconds: u16, + pub has_video_references: bool, + pub total_input_seconds: u16, +} + +impl ArtcraftSeedance2p5UltraCostState { + pub fn from_request(request: &ArtcraftSeedance2p5UltraRequestState) -> Self { + let resolution = request.request.resolution + .unwrap_or(CommonResolution::SevenTwentyP); + let duration_seconds = request.request.duration_seconds.unwrap_or(5); + + let has_video_references = request.request.reference_video_media_tokens + .as_ref() + .is_some_and(|tokens| !tokens.is_empty()); + + Self { + resolution, + duration_seconds, + has_video_references, + total_input_seconds: request.total_input_seconds.unwrap_or(0), + } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + let total_input_seconds = self.total_input_seconds + .min(u16::from(MAX_BILLED_INPUT_SECONDS)); + + let usd_cents = seedance_2p5_ultra_usd_cents( + self.resolution, + self.duration_seconds, + self.has_video_references, + total_input_seconds, + ); + + // ArtCraft credits: 100 credits = $1.00, so credits = cents. + VideoGenerationCostEstimate { + cost_in_credits: Some(usd_cents), + cost_in_usd_cents: Some(usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + failures_are_refunded: None, + } + } +} + +#[cfg(test)] +mod tests { + use crate::api::router_provider::RouterProvider; + use crate::api::router_resolution::RouterResolution; + use crate::api::router_video_model::RouterVideoModel; + use crate::api::video_list_ref::VideoListRef; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; + + use tokens::tokens::media_files::MediaFileToken; + + mod pricing_without_video_references { + use super::*; + + #[test] + fn table_480p() { + // 13.90946502 ¢/s, rounded up. + assert_eq!(cents(Some(RouterResolution::FourEightyP), 4), 56); + assert_eq!(cents(Some(RouterResolution::FourEightyP), 5), 70); + assert_eq!(cents(Some(RouterResolution::FourEightyP), 10), 140); + assert_eq!(cents(Some(RouterResolution::FourEightyP), 30), 418); + } + + #[test] + fn table_720p() { + // 31.56378601 ¢/s, rounded up. + assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 4), 127); + assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 5), 158); + assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 10), 316); + assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 30), 947); + } + + #[test] + fn none_defaults_to_720p() { + assert_eq!(cents(None, 10), cents(Some(RouterResolution::SevenTwentyP), 10)); + } + + #[test] + fn ultra_prices_above_regular_seedance_2p5() { + for (resolution, duration) in [ + (RouterResolution::FourEightyP, 5u16), + (RouterResolution::FourEightyP, 30), + (RouterResolution::SevenTwentyP, 5), + (RouterResolution::SevenTwentyP, 30), + ] { + let ultra = cents(Some(resolution), duration); + let regular = estimate_for_model(RouterVideoModel::Seedance2p5, |b| { + b.resolution = Some(resolution); + b.duration_seconds = Some(duration); + }).cost_in_usd_cents.unwrap(); + assert!(ultra > regular, "{resolution:?} {duration}s: ultra {ultra} <= regular {regular}"); + } + } + } + + mod pricing_with_video_references { + use super::*; + + #[test] + fn thirty_second_output_with_ten_input_seconds_bills_forty() { + // 8.55967078 ¢/s × 40 = 342.39 → 343¢. + assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(10)), 343); + // 18.72427984 ¢/s × 40 = 748.97 → 749¢. + assert_eq!(cents_with_video_refs(Some(RouterResolution::SevenTwentyP), 30, Some(10)), 749); + } + + #[test] + fn fourteen_input_seconds_bill_forty_four() { + // 8.55967078 ¢/s × 44 = 376.63 → 377¢. + assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(14)), 377); + } + + #[test] + fn missing_input_seconds_bill_output_duration_only() { + // 8.55967078 ¢/s × 10 = 85.60 → 86¢. + assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 10, None), 86); + } + + #[test] + fn input_seconds_clamp_to_max_billed_input_seconds() { + // 200 input seconds clamp to 30: 8.55967078 ¢/s × (30+30) = 513.58 → 514¢. + assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(200)), 514); + assert_eq!( + cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(200)), + cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(30)), + ); + } + } + + mod credits_tests { + use super::*; + + #[test] + fn credits_equal_usd_cents() { + for dur in [4u16, 10, 30] { + let estimate = estimate_with(|b| { + b.resolution = Some(RouterResolution::FourEightyP); + b.duration_seconds = Some(dur); + }); + assert_eq!(estimate.cost_in_credits, estimate.cost_in_usd_cents); + } + } + } + + // ── Helpers ── + + fn estimate_with(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> VideoGenerationCostEstimate { + estimate_for_model(RouterVideoModel::Seedance2p5Ultra, f) + } + + fn estimate_for_model( + model: RouterVideoModel, + f: impl FnOnce(&mut GenerateVideoRequestBuilder), + ) -> VideoGenerationCostEstimate { + let mut builder = GenerateVideoRequestBuilder { + model, + provider: RouterProvider::Artcraft, + duration_seconds: Some(5), + video_batch_count: Some(1), + ..Default::default() + }; + f(&mut builder); + builder.build2() + .expect("build should succeed") + .estimate_cost() + .expect("estimate should succeed") + } + + fn cents(resolution: Option, duration_seconds: u16) -> u64 { + estimate_with(|b| { + b.resolution = resolution; + b.duration_seconds = Some(duration_seconds); + }).cost_in_usd_cents.unwrap() + } + + fn cents_with_video_refs( + resolution: Option, + duration_seconds: u16, + total_input_seconds: Option, + ) -> u64 { + estimate_with(|b| { + b.resolution = resolution; + b.duration_seconds = Some(duration_seconds); + b.reference_videos = Some(VideoListRef::MediaFileTokens(vec![ + MediaFileToken::new("mf_ref".to_string()), + ])); + b.total_reference_video_input_seconds = total_input_seconds; + }).cost_in_usd_cents.unwrap() + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/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/providers/artcraft/seedance_2p5_u/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/request.rs new file mode 100644 index 0000000000..a2952414e2 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5_u/request.rs @@ -0,0 +1,22 @@ +use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest; + +use crate::client::router_artcraft_client::RouterArtcraftClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; +use crate::generate::generate_video::providers::artcraft::request_common::send_artcraft_omni_video_request; + +#[derive(Clone, Debug)] +pub struct ArtcraftSeedance2p5UltraRequestState { + pub request: OmniGenVideoCostAndGenerateRequest, + + /// Calculation-only: total seconds of reference video input, billed on top + /// of the output duration when reference videos are attached. Not part of + /// the omni request wire shape. + pub total_input_seconds: Option, +} + +impl ArtcraftSeedance2p5UltraRequestState { + pub async fn send(&self, client: &RouterArtcraftClient) -> Result { + send_artcraft_omni_video_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_common.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_common.rs index b2b5b859c2..d8a755d92e 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_common.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_common.rs @@ -181,6 +181,48 @@ pub fn seedance_2p5_usd_cents( (cents_per_second * billed_seconds as f64).ceil() as u64 } +/// Seedance 2.5 Ultra — 480p price, USD cents per second. +const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_480P: f64 = 13.90946502; +/// Seedance 2.5 Ultra — 720p price, USD cents per second. +const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_720P: f64 = 31.56378601; +/// Seedance 2.5 Ultra — 480p price with video references, USD cents per second. +const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_480P: f64 = 8.55967078; +/// Seedance 2.5 Ultra — 720p price with video references, USD cents per second. +const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_720P: f64 = 18.72427984; + +/// ArtCraft's price (USD cents) for Seedance 2.5 Ultra (the BytePlus Ultra +/// kinovi account routing of Seedance 2.5). +/// +/// Same billing structure as [`seedance_2p5_usd_cents`], at higher rates: +/// only 480p and 720p are offered; any other resolution prices at 720p. +/// Without video references, billed seconds = output duration. With video +/// references, the per-second rate drops but the billed seconds are the +/// output duration PLUS the total seconds of reference video input. The +/// fractional total is rounded UP to a whole cent. No batching. +pub fn seedance_2p5_ultra_usd_cents( + resolution: CommonResolution, + duration_seconds: u16, + has_video_references: bool, + total_input_seconds: u16, +) -> u64 { + let (cents_per_second, billed_seconds) = if has_video_references { + let rate = match resolution { + CommonResolution::FourEightyP => SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_480P, + // Everything else (including 720p and unsupported resolutions) prices at 720p. + _ => SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_720P, + }; + (rate, u64::from(duration_seconds) + u64::from(total_input_seconds)) + } else { + let rate = match resolution { + CommonResolution::FourEightyP => SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_480P, + _ => SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_720P, + }; + (rate, u64::from(duration_seconds)) + }; + + (cents_per_second * billed_seconds as f64).ceil() as u64 +} + #[allow(clippy::too_many_arguments)] fn mini_usd_cents( resolution: CommonResolution, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_request.rs index 2ef90a8ad7..9443d5ebf6 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_request.rs @@ -57,6 +57,8 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p0_mini::cos use crate::generate::generate_video::providers::artcraft::seedance_2p0_mini::request::ArtcraftSeedance2p0MiniRequestState; use crate::generate::generate_video::providers::artcraft::seedance_2p5::cost::ArtcraftSeedance2p5CostState; use crate::generate::generate_video::providers::artcraft::seedance_2p5::request::ArtcraftSeedance2p5RequestState; +use crate::generate::generate_video::providers::artcraft::seedance_2p5_u::cost::ArtcraftSeedance2p5UltraCostState; +use crate::generate::generate_video::providers::artcraft::seedance_2p5_u::request::ArtcraftSeedance2p5UltraRequestState; use crate::generate::generate_video::providers::artcraft::seedance_2p5_preview::cost::ArtcraftSeedance2p5PreviewCostState; use crate::generate::generate_video::providers::artcraft::seedance_2p5_preview::request::ArtcraftSeedance2p5PreviewRequestState; use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_mini::cost::ArtcraftSeedance2p0BytePlusMiniCostState; @@ -178,6 +180,7 @@ pub enum VideoGenerationRequest { ArtcraftSeedance2p0Mini(ArtcraftSeedance2p0MiniRequestState), ArtcraftSeedance2p5Preview(ArtcraftSeedance2p5PreviewRequestState), ArtcraftSeedance2p5(ArtcraftSeedance2p5RequestState), + ArtcraftSeedance2p5Ultra(ArtcraftSeedance2p5UltraRequestState), ArtcraftSeedance2p0BytePlusMini(ArtcraftSeedance2p0BytePlusMiniRequestState), ArtcraftSeedance2p0BytePlusUltraMini(ArtcraftSeedance2p0BytePlusUltraMiniRequestState), ArtcraftSora2(ArtcraftSora2RequestState), @@ -256,6 +259,7 @@ impl VideoGenerationRequest { Self::ArtcraftSeedance2p0Mini(_) => RouterProvider::Artcraft, Self::ArtcraftSeedance2p5Preview(_) => RouterProvider::Artcraft, Self::ArtcraftSeedance2p5(_) => RouterProvider::Artcraft, + Self::ArtcraftSeedance2p5Ultra(_) => RouterProvider::Artcraft, Self::ArtcraftSeedance2p0BytePlusMini(_) => RouterProvider::Artcraft, Self::ArtcraftSeedance2p0BytePlusUltraMini(_) => RouterProvider::Artcraft, Self::ArtcraftSora2(_) => RouterProvider::Artcraft, @@ -334,6 +338,7 @@ impl VideoGenerationRequest { VideoGenerationRequest::ArtcraftSeedance2p0Mini(request) => Ok(ArtcraftSeedance2p0MiniCostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSeedance2p5Preview(request) => Ok(ArtcraftSeedance2p5PreviewCostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSeedance2p5(request) => Ok(ArtcraftSeedance2p5CostState::from_request(request).estimate_cost()), + VideoGenerationRequest::ArtcraftSeedance2p5Ultra(request) => Ok(ArtcraftSeedance2p5UltraCostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSeedance2p0BytePlusMini(request) => Ok(ArtcraftSeedance2p0BytePlusMiniCostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltraMini(request) => Ok(ArtcraftSeedance2p0BytePlusUltraMiniCostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSora2(request) => Ok(ArtcraftSora2CostState::from_request(request).estimate_cost()), @@ -497,6 +502,10 @@ impl VideoGenerationRequest { let client_ref = client.get_artcraft_client_ref()?; request.send(client_ref).await }, + VideoGenerationRequest::ArtcraftSeedance2p5Ultra(request) => { + let client_ref = client.get_artcraft_client_ref()?; + request.send(client_ref).await + }, VideoGenerationRequest::ArtcraftSeedance2p0BytePlusMini(request) => { let client_ref = client.get_artcraft_client_ref()?; request.send(client_ref).await diff --git a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_model_type.rs b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_model_type.rs index 8cef5dda09..44149d1853 100644 --- a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_model_type.rs +++ b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_model_type.rs @@ -222,6 +222,9 @@ pub enum InferenceModelType { Seedance2p5Preview, #[serde(rename = "seedance_2p5")] Seedance2p5, + /// Seedance 2.5 as routed via the BytePlus Ultra kinovi account + #[serde(rename = "seedance_2p5_u")] + Seedance2p5Ultra, #[serde(rename = "sora_2")] Sora2, #[serde(rename = "sora_2_pro")] @@ -399,6 +402,7 @@ impl InferenceModelType { Self::Seedance2p0BytePlusUltraMini => "seedance_2p0_bpu_mini", Self::Seedance2p5Preview => "seedance_2p5_preview", Self::Seedance2p5 => "seedance_2p5", + Self::Seedance2p5Ultra => "seedance_2p5_u", Self::Sora2 => "sora_2", Self::Sora2Pro => "sora_2_pro", Self::Veo2 => "veo_2", @@ -536,6 +540,7 @@ impl InferenceModelType { "seedance_2p0_bpu_mini" => Ok(Self::Seedance2p0BytePlusUltraMini), "seedance_2p5_preview" => Ok(Self::Seedance2p5Preview), "seedance_2p5" => Ok(Self::Seedance2p5), + "seedance_2p5_u" => Ok(Self::Seedance2p5Ultra), "sora_2" => Ok(Self::Sora2), "sora_2_pro" => Ok(Self::Sora2Pro), "veo_2" => Ok(Self::Veo2), @@ -677,6 +682,7 @@ impl InferenceModelType { Self::Seedance2p0BytePlusUltraMini, Self::Seedance2p5Preview, Self::Seedance2p5, + Self::Seedance2p5Ultra, Self::Sora2, Self::Sora2Pro, Self::Veo2, @@ -801,6 +807,7 @@ impl InferenceModelType { CommonModelType::Seedance2p0BytePlusUltraMini => Self::Seedance2p0BytePlusUltraMini, CommonModelType::Seedance2p5Preview => Self::Seedance2p5Preview, CommonModelType::Seedance2p5 => Self::Seedance2p5, + CommonModelType::Seedance2p5Ultra => Self::Seedance2p5Ultra, CommonModelType::Sora2 => Self::Sora2, CommonModelType::Sora2Pro => Self::Sora2Pro, CommonModelType::Veo2 => Self::Veo2, @@ -947,6 +954,7 @@ mod tests { assert_serialization(InferenceModelType::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini"); assert_serialization(InferenceModelType::Seedance2p5Preview, "seedance_2p5_preview"); assert_serialization(InferenceModelType::Seedance2p5, "seedance_2p5"); + assert_serialization(InferenceModelType::Seedance2p5Ultra, "seedance_2p5_u"); assert_serialization(InferenceModelType::Sora2, "sora_2"); assert_serialization(InferenceModelType::Sora2Pro, "sora_2_pro"); assert_serialization(InferenceModelType::Veo2, "veo_2"); @@ -1080,6 +1088,7 @@ mod tests { assert_eq!(InferenceModelType::Seedance2p0BytePlusUltraMini.to_str(), "seedance_2p0_bpu_mini"); assert_eq!(InferenceModelType::Seedance2p5Preview.to_str(), "seedance_2p5_preview"); assert_eq!(InferenceModelType::Seedance2p5.to_str(), "seedance_2p5"); + assert_eq!(InferenceModelType::Seedance2p5Ultra.to_str(), "seedance_2p5_u"); assert_eq!(InferenceModelType::Sora2.to_str(), "sora_2"); assert_eq!(InferenceModelType::Sora2Pro.to_str(), "sora_2_pro"); assert_eq!(InferenceModelType::Veo2.to_str(), "veo_2"); @@ -1215,6 +1224,7 @@ mod tests { assert_eq!(InferenceModelType::from_str("seedance_2p0_bpu_mini").unwrap(), InferenceModelType::Seedance2p0BytePlusUltraMini); assert_eq!(InferenceModelType::from_str("seedance_2p5_preview").unwrap(), InferenceModelType::Seedance2p5Preview); assert_eq!(InferenceModelType::from_str("seedance_2p5").unwrap(), InferenceModelType::Seedance2p5); + assert_eq!(InferenceModelType::from_str("seedance_2p5_u").unwrap(), InferenceModelType::Seedance2p5Ultra); assert_eq!(InferenceModelType::from_str("sora_2").unwrap(), InferenceModelType::Sora2); assert_eq!(InferenceModelType::from_str("sora_2_pro").unwrap(), InferenceModelType::Sora2Pro); assert_eq!(InferenceModelType::from_str("veo_2").unwrap(), InferenceModelType::Veo2); diff --git a/crates/schema/public/enums/src/common/generation/common_model_type.rs b/crates/schema/public/enums/src/common/generation/common_model_type.rs index 8e569a6853..4d4ccf1b36 100644 --- a/crates/schema/public/enums/src/common/generation/common_model_type.rs +++ b/crates/schema/public/enums/src/common/generation/common_model_type.rs @@ -193,6 +193,9 @@ pub enum CommonModelType { Seedance2p5Preview, #[serde(rename = "seedance_2p5")] Seedance2p5, + /// Seedance 2.5 as routed via the BytePlus Ultra kinovi account + #[serde(rename = "seedance_2p5_u")] + Seedance2p5Ultra, #[serde(rename = "sora_2")] Sora2, #[serde(rename = "sora_2_pro")] @@ -354,6 +357,7 @@ impl CommonModelType { Self::Seedance2p0BytePlusUltraMini => "seedance_2p0_bpu_mini", Self::Seedance2p5Preview => "seedance_2p5_preview", Self::Seedance2p5 => "seedance_2p5", + Self::Seedance2p5Ultra => "seedance_2p5_u", Self::Sora2 => "sora_2", Self::Sora2Pro => "sora_2_pro", Self::Veo2 => "veo_2", @@ -474,6 +478,7 @@ impl CommonModelType { "seedance_2p0_bpu_mini" => Ok(Self::Seedance2p0BytePlusUltraMini), "seedance_2p5_preview" => Ok(Self::Seedance2p5Preview), "seedance_2p5" => Ok(Self::Seedance2p5), + "seedance_2p5_u" => Ok(Self::Seedance2p5Ultra), "sora_2" => Ok(Self::Sora2), "sora_2_pro" => Ok(Self::Sora2Pro), "veo_2" => Ok(Self::Veo2), @@ -598,6 +603,7 @@ impl CommonModelType { Self::Seedance2p0BytePlusUltraMini, Self::Seedance2p5Preview, Self::Seedance2p5, + Self::Seedance2p5Ultra, Self::Sora2, Self::Sora2Pro, Self::Veo2, @@ -719,6 +725,7 @@ impl CommonModelType { Self::Seedance2p0BytePlusUltraMini => CommonModelClass::Video, Self::Seedance2p5Preview => CommonModelClass::Video, Self::Seedance2p5 => CommonModelClass::Video, + Self::Seedance2p5Ultra => CommonModelClass::Video, Self::Sora2 => CommonModelClass::Video, Self::Sora2Pro => CommonModelClass::Video, Self::Veo2 => CommonModelClass::Video, @@ -849,6 +856,7 @@ mod tests { assert_serialization(CommonModelType::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini"); assert_serialization(CommonModelType::Seedance2p5Preview, "seedance_2p5_preview"); assert_serialization(CommonModelType::Seedance2p5, "seedance_2p5"); + assert_serialization(CommonModelType::Seedance2p5Ultra, "seedance_2p5_u"); assert_serialization(CommonModelType::Sora2, "sora_2"); assert_serialization(CommonModelType::Sora2Pro, "sora_2_pro"); assert_serialization(CommonModelType::Veo2, "veo_2"); @@ -964,6 +972,7 @@ mod tests { assert_eq!(CommonModelType::Seedance2p0BytePlusUltraMini.to_str(), "seedance_2p0_bpu_mini"); assert_eq!(CommonModelType::Seedance2p5Preview.to_str(), "seedance_2p5_preview"); assert_eq!(CommonModelType::Seedance2p5.to_str(), "seedance_2p5"); + assert_eq!(CommonModelType::Seedance2p5Ultra.to_str(), "seedance_2p5_u"); assert_eq!(CommonModelType::Sora2.to_str(), "sora_2"); assert_eq!(CommonModelType::Sora2Pro.to_str(), "sora_2_pro"); assert_eq!(CommonModelType::Veo2.to_str(), "veo_2"); @@ -1081,6 +1090,7 @@ mod tests { assert_eq!(CommonModelType::from_str("seedance_2p0_bpu_mini").unwrap(), CommonModelType::Seedance2p0BytePlusUltraMini); assert_eq!(CommonModelType::from_str("seedance_2p5_preview").unwrap(), CommonModelType::Seedance2p5Preview); assert_eq!(CommonModelType::from_str("seedance_2p5").unwrap(), CommonModelType::Seedance2p5); + assert_eq!(CommonModelType::from_str("seedance_2p5_u").unwrap(), CommonModelType::Seedance2p5Ultra); assert_eq!(CommonModelType::from_str("sora_2").unwrap(), CommonModelType::Sora2); assert_eq!(CommonModelType::from_str("sora_2_pro").unwrap(), CommonModelType::Sora2Pro); assert_eq!(CommonModelType::from_str("veo_2").unwrap(), CommonModelType::Veo2); @@ -1125,7 +1135,7 @@ mod tests { #[test] fn all_variants() { let mut variants = CommonModelType::all_variants(); - assert_eq!(variants.len(), 105); + assert_eq!(variants.len(), 106); // Image models assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Dev)); assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Schnell)); @@ -1198,6 +1208,7 @@ mod tests { assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0BytePlusUltraMini)); assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p5Preview)); assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p5)); + assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p5Ultra)); assert_eq!(variants.pop_first(), Some(CommonModelType::Sora2)); assert_eq!(variants.pop_first(), Some(CommonModelType::Sora2Pro)); assert_eq!(variants.pop_first(), Some(CommonModelType::Veo2)); diff --git a/crates/schema/public/enums/src/common/generation/common_video_model.rs b/crates/schema/public/enums/src/common/generation/common_video_model.rs index 1c55436dad..65acf96ea5 100644 --- a/crates/schema/public/enums/src/common/generation/common_video_model.rs +++ b/crates/schema/public/enums/src/common/generation/common_video_model.rs @@ -101,6 +101,11 @@ pub enum CommonVideoModel { #[serde(rename = "seedance_2p5")] Seedance2p5, + /// Seedance 2.5 as routed via the BytePlus Ultra kinovi account. + /// Fulfilled by Seedance 2.5; billed at its own (higher) rate. + #[serde(rename = "seedance_2p5_u")] + Seedance2p5Ultra, + #[serde(rename = "sora_2")] Sora2, @@ -177,6 +182,7 @@ impl CommonVideoModel { Self::Seedance2p0BytePlusUltraMini => CommonModelType::Seedance2p0BytePlusUltraMini, Self::Seedance2p5Preview => CommonModelType::Seedance2p5Preview, Self::Seedance2p5 => CommonModelType::Seedance2p5, + Self::Seedance2p5Ultra => CommonModelType::Seedance2p5Ultra, Self::Sora2 => CommonModelType::Sora2, Self::Sora2Pro => CommonModelType::Sora2Pro, Self::Veo2 => CommonModelType::Veo2, @@ -232,6 +238,7 @@ mod tests { assert_serialization(CommonVideoModel::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini"); assert_serialization(CommonVideoModel::Seedance2p5Preview, "seedance_2p5_preview"); assert_serialization(CommonVideoModel::Seedance2p5, "seedance_2p5"); + assert_serialization(CommonVideoModel::Seedance2p5Ultra, "seedance_2p5_u"); assert_serialization(CommonVideoModel::Sora2, "sora_2"); assert_serialization(CommonVideoModel::Sora2Pro, "sora_2_pro"); assert_serialization(CommonVideoModel::Veo2, "veo_2"); @@ -280,6 +287,7 @@ mod tests { ("seedance_2p0_bpu_mini", CommonVideoModel::Seedance2p0BytePlusUltraMini), ("seedance_2p5_preview", CommonVideoModel::Seedance2p5Preview), ("seedance_2p5", CommonVideoModel::Seedance2p5), + ("seedance_2p5_u", CommonVideoModel::Seedance2p5Ultra), ("sora_2", CommonVideoModel::Sora2), ("sora_2_pro", CommonVideoModel::Sora2Pro), ("veo_2", CommonVideoModel::Veo2), @@ -335,6 +343,7 @@ mod tests { CommonVideoModel::Seedance2p0BytePlusUltraMini, CommonVideoModel::Seedance2p5Preview, CommonVideoModel::Seedance2p5, + CommonVideoModel::Seedance2p5Ultra, CommonVideoModel::Sora2, CommonVideoModel::Sora2Pro, CommonVideoModel::Veo2, @@ -389,6 +398,7 @@ mod tests { (CommonVideoModel::Seedance2p0BytePlusUltraMini, CommonModelType::Seedance2p0BytePlusUltraMini), (CommonVideoModel::Seedance2p5Preview, CommonModelType::Seedance2p5Preview), (CommonVideoModel::Seedance2p5, CommonModelType::Seedance2p5), + (CommonVideoModel::Seedance2p5Ultra, CommonModelType::Seedance2p5Ultra), (CommonVideoModel::Sora2, CommonModelType::Sora2), (CommonVideoModel::Sora2Pro, CommonModelType::Sora2Pro), (CommonVideoModel::Veo2, CommonModelType::Veo2), diff --git a/crates/service/web/storyteller_web/src/configs/omni_gen/video_models/by_type/seedance_2p5_video_models.rs b/crates/service/web/storyteller_web/src/configs/omni_gen/video_models/by_type/seedance_2p5_video_models.rs index 96cb1c849d..7411423a46 100644 --- a/crates/service/web/storyteller_web/src/configs/omni_gen/video_models/by_type/seedance_2p5_video_models.rs +++ b/crates/service/web/storyteller_web/src/configs/omni_gen/video_models/by_type/seedance_2p5_video_models.rs @@ -49,6 +49,47 @@ pub fn seedance_2p5_video_models() -> Vec { ..Default::default() }); + models.push(OmniGenVideoModelDetails { + model: CommonVideoModel::Seedance2p5Ultra, + model_creator: Some(ModelCreator::Bytedance), + full_name: Some("Seedance 2.5 Ultra".to_string()), + extra_info: Some("Seedance 2.5 Ultra. Supports videos up to 30 seconds, start/end keyframes (adaptive aspect ratio), and image/video/audio references, with 480p/720p output. Video references bill their input seconds on top of the output duration.".to_string()), + extra_info_short: Some("Seedance 2.5 Ultra; up to 30s videos".to_string()), + text_prompt_supported: Some(true), + text_prompt_max_length: Some(10_000), + starting_keyframe_supported: Some(true), + ending_keyframe_supported: Some(true), + image_references_supported: Some(true), + image_references_max: Some(30), + audio_references_supported: Some(true), + audio_references_max: Some(10), + audio_references_max_total_duration_seconds: Some(30), + video_references_supported: Some(true), + video_references_max: Some(10), + video_references_max_total_duration_seconds: Some(30), + character_references_supported: Some(false), + aspect_ratio_options: Some(vec![ + CommonAspectRatio::WideTwentyOneByNine, + CommonAspectRatio::WideSixteenByNine, + CommonAspectRatio::WideFourByThree, + CommonAspectRatio::Square, + CommonAspectRatio::TallThreeByFour, + CommonAspectRatio::TallNineBySixteen, + ]), + aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine), + resolution_options: Some(vec![ + CommonResolution::FourEightyP, + CommonResolution::SevenTwentyP, + ]), + resolution_default: Some(CommonResolution::SevenTwentyP), + batch_size_options: Some(vec![1]), + batch_size_default: Some(1), + duration_seconds_min: Some(4), + duration_seconds_max: Some(30), + duration_seconds_default: Some(5), + ..Default::default() + }); + models.push(OmniGenVideoModelDetails { model: CommonVideoModel::Seedance2p5Preview, model_creator: Some(ModelCreator::Bytedance), diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/video/estimate_video_cost_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/video/estimate_video_cost_handler.rs index 4784538c91..b5bcc21791 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/video/estimate_video_cost_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/video/estimate_video_cost_handler.rs @@ -158,6 +158,7 @@ fn map_video_model(model: CommonVideoModel) -> Result RouterVideoModel::Seedance2p0BytePlusUltraMini, CommonVideoModel::Seedance2p5Preview => RouterVideoModel::Seedance2p5Preview, CommonVideoModel::Seedance2p5 => RouterVideoModel::Seedance2p5, + CommonVideoModel::Seedance2p5Ultra => RouterVideoModel::Seedance2p5Ultra, CommonVideoModel::HappyHorse1p0 => RouterVideoModel::HappyHorse1p0, CommonVideoModel::MinimaxH3 => RouterVideoModel::MinimaxH3, CommonVideoModel::MinimaxH3Turbo => RouterVideoModel::MinimaxH3Turbo, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/omni_api_video_generate_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/omni_api_video_generate_handler.rs index 11a40cb0cd..46d809cf67 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/omni_api_video_generate_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/omni_api_video_generate_handler.rs @@ -193,7 +193,7 @@ pub async fn omni_api_video_generate_handler( // instead of downloading the same bytes twice. let mut maybe_probed_reference_videos: Option = None; - if matches!(request.model, Some(CommonVideoModel::Seedance2p5)) { + if matches!(request.model, Some(CommonVideoModel::Seedance2p5 | CommonVideoModel::Seedance2p5Ultra)) { if let Some(video_tokens) = request.reference_video_media_tokens.as_deref().filter(|tokens| !tokens.is_empty()) { let video_sources = fetch_reference_video_sources( video_tokens, @@ -226,6 +226,7 @@ pub async fn omni_api_video_generate_handler( Some(CommonVideoModel::Seedance2p0BytePlusUltra) => KinoviAccount::BytePlusUltra, Some(CommonVideoModel::Seedance2p0BytePlusUltraFast) => KinoviAccount::BytePlusUltra, Some(CommonVideoModel::Seedance2p0BytePlusUltraMini) => KinoviAccount::BytePlusUltra, + Some(CommonVideoModel::Seedance2p5Ultra) => KinoviAccount::BytePlusUltra, // BytePlus Some(CommonVideoModel::Seedance2p0BytePlus) => KinoviAccount::BytePlus, Some(CommonVideoModel::Seedance2p0BytePlusFast) => KinoviAccount::BytePlus, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/pipeline_v2/run_pipeline_v2.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/pipeline_v2/run_pipeline_v2.rs index 61b40f719f..16e776d644 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/pipeline_v2/run_pipeline_v2.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/pipeline_v2/run_pipeline_v2.rs @@ -89,6 +89,7 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p5Preview => RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p5 => RouterProvider::KinoviWeb, + RouterVideoModel::Seedance2p5Ultra => RouterProvider::KinoviWeb, //RouterVideoModel::Seedance2p0Ultra => RouterProvider::GmiCloud, //RouterVideoModel::Seedance2p0UltraFast => RouterProvider::GmiCloud, RouterVideoModel::GrokImagineVideo => RouterProvider::GrokApi, @@ -100,6 +101,15 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result = None; - if matches!(request.model, Some(CommonVideoModel::Seedance2p5)) { + if matches!(request.model, Some(CommonVideoModel::Seedance2p5 | CommonVideoModel::Seedance2p5Ultra)) { if let Some(video_tokens) = request.reference_video_media_tokens.as_deref().filter(|tokens| !tokens.is_empty()) { let video_sources = fetch_reference_video_sources( video_tokens, @@ -209,6 +209,7 @@ pub async fn omni_gen_video_generate_handler( Some(CommonVideoModel::Seedance2p0BytePlusUltra) => KinoviAccount::BytePlusUltra, Some(CommonVideoModel::Seedance2p0BytePlusUltraFast) => KinoviAccount::BytePlusUltra, Some(CommonVideoModel::Seedance2p0BytePlusUltraMini) => KinoviAccount::BytePlusUltra, + Some(CommonVideoModel::Seedance2p5Ultra) => KinoviAccount::BytePlusUltra, // BytePlus Some(CommonVideoModel::Seedance2p0BytePlus) => KinoviAccount::BytePlus, Some(CommonVideoModel::Seedance2p0BytePlusFast) => KinoviAccount::BytePlus, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/pipeline_v2/run_pipeline_v2.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/pipeline_v2/run_pipeline_v2.rs index be9e5fc319..86da93b428 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/pipeline_v2/run_pipeline_v2.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/pipeline_v2/run_pipeline_v2.rs @@ -90,6 +90,7 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p5Preview => RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p5 => RouterProvider::KinoviWeb, + RouterVideoModel::Seedance2p5Ultra => RouterProvider::KinoviWeb, //RouterVideoModel::Seedance2p0Ultra => RouterProvider::GmiCloud, //RouterVideoModel::Seedance2p0UltraFast => RouterProvider::GmiCloud, RouterVideoModel::GrokImagineVideo => RouterProvider::GrokApi, @@ -101,6 +102,15 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result