diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/omni_gen/cost_and_generate_requests/omni_gen_video_cost_and_generate_request.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/omni_gen/cost_and_generate_requests/omni_gen_video_cost_and_generate_request.rs index 8106b25a24..07f91a32b8 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/omni_gen/cost_and_generate_requests/omni_gen_video_cost_and_generate_request.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/omni_gen/cost_and_generate_requests/omni_gen_video_cost_and_generate_request.rs @@ -2,6 +2,7 @@ use serde_derive::{Deserialize, Serialize}; use utoipa::ToSchema; use enums::common::generation::common_aspect_ratio::CommonAspectRatio; +use enums::common::generation::common_bitrate::CommonBitrate; use enums::common::generation::common_quality::CommonQuality; use enums::common::generation::common_resolution::CommonResolution; use enums::common::generation::common_video_model::CommonVideoModel; @@ -50,6 +51,10 @@ pub struct OmniGenVideoCostAndGenerateRequest { /// The aspect ratio to use. pub aspect_ratio: Option, + /// The output bitrate to use. + /// Not all models support this; models that don't simply ignore it. + pub bitrate: Option, + /// The quality to use. pub quality: Option, diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/omni_gen/models/omni_gen_video_models.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/omni_gen/models/omni_gen_video_models.rs index 8ae16e56e7..8cbea2f5e0 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/omni_gen/models/omni_gen_video_models.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/omni_gen/models/omni_gen_video_models.rs @@ -6,6 +6,7 @@ use enums::common::generation::model_creator::ModelCreator; use enums::common::generation_provider::GenerationProvider; use serde_derive::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; +use enums::common::generation::common_bitrate::CommonBitrate; /// Video model to default to if none is specified const DEFAULT_VIDEO_MODEL : CommonVideoModel = CommonVideoModel::Seedance2p0; @@ -150,6 +151,12 @@ pub struct OmniGenVideoModelDetails { #[serde(skip_serializing_if = "Option::is_none")] pub resolution_default: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bitrate_options: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub bitrate_default: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub quality_options: Option>, @@ -220,6 +227,8 @@ impl Default for OmniGenVideoModelDetails { aspect_ratio_default: None, resolution_options: None, resolution_default: None, + bitrate_options: None, + bitrate_default: None, quality_options: None, default_quality: None, duration_seconds_min: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/api/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/api/mod.rs index fa7e90dfa5..1e3ded5a95 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/api/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/api/mod.rs @@ -3,6 +3,7 @@ pub mod character_list_ref; pub mod image_list_ref; pub mod image_ref; pub mod router_aspect_ratio; +pub mod router_bitrate; pub mod router_image_model; pub mod router_provider; pub mod router_quality; diff --git a/crates/api_clients/artcraft/artcraft_router/src/api/router_bitrate.rs b/crates/api_clients/artcraft/artcraft_router/src/api/router_bitrate.rs new file mode 100644 index 0000000000..ae9806a79f --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/api/router_bitrate.rs @@ -0,0 +1,10 @@ +use serde_derive::{Deserialize, Serialize}; + +/// Output bitrate levels you can specify when enqueuing a generation. +/// Not every model will use this; models that don't simply ignore it. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum RouterBitrate { + Normal, + High, +} 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 b9b98d7afb..70e21a6258 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 @@ -1,6 +1,7 @@ use crate::api::audio_list_ref::AudioListRef; use crate::api::character_list_ref::CharacterListRef; use crate::api::router_aspect_ratio::RouterAspectRatio; +use crate::api::router_bitrate::RouterBitrate; use crate::api::router_resolution::RouterResolution; use crate::api::router_video_model::RouterVideoModel; use crate::api::image_list_ref::ImageListRef; @@ -103,6 +104,10 @@ pub struct GenerateVideoRequestBuilder { /// The aspect ratio to use pub aspect_ratio: Option, + /// The output bitrate to use. + /// Not all models support this; models that don't simply ignore it. + pub bitrate: Option, + /// How many seconds to generate. pub duration_seconds: Option, @@ -139,6 +144,7 @@ impl Default for GenerateVideoRequestBuilder { reference_character_tokens: None, resolution: None, aspect_ratio: None, + bitrate: None, duration_seconds: None, video_batch_count: None, generate_audio: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/build_common.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/build_common.rs index 35fb146521..7d7d6a5924 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/build_common.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/build_common.rs @@ -1,9 +1,11 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest; use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum; +use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum; use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; use crate::api::router_aspect_ratio::RouterAspectRatio; +use crate::api::router_bitrate::RouterBitrate; use crate::api::router_resolution::RouterResolution; use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; use crate::errors::artcraft_router_error::ArtcraftRouterError; @@ -49,6 +51,7 @@ pub fn build_artcraft_omni_video_request( let resolution = plan_output_resolution(builder.resolution.take(), strategy, resolutions)?; let batch_count = plan_batch_count(builder.video_batch_count.take(), strategy)?; let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?; + let bitrate = plan_bitrate(builder.bitrate.take()); let prompt = builder.prompt.take(); let start_frame = resolve_image_ref(builder.start_frame.take())?; @@ -71,6 +74,7 @@ pub fn build_artcraft_omni_video_request( reference_character_tokens: reference_characters, resolution, aspect_ratio, + bitrate, duration_seconds: duration_seconds.map(|d| d as u16), video_batch_count: Some(batch_count), negative_prompt: None, @@ -234,6 +238,19 @@ pub fn plan_batch_count( } } +/// Translate the router-facing bitrate into the API `CommonBitrate`. +/// +/// Bitrate does not affect cost. An unset value leaves the field `None` (the +/// API applies its default); `Normal` and `High` map to their `CommonBitrate` +/// counterparts. +fn plan_bitrate(bitrate: Option) -> Option { + match bitrate { + None => None, + Some(RouterBitrate::Normal) => Some(CommonBitrateEnum::Normal), + Some(RouterBitrate::High) => Some(CommonBitrateEnum::High), + } +} + /// Duration: 4-15 seconds. pub fn plan_duration( duration_seconds: Option, @@ -255,3 +272,27 @@ pub fn plan_duration( }, } } + +#[cfg(test)] +mod tests { + use super::*; + + mod bitrate_translation { + use super::*; + + #[test] + fn none_stays_none() { + assert_eq!(plan_bitrate(None), None); + } + + #[test] + fn normal_maps_to_common_normal() { + assert_eq!(plan_bitrate(Some(RouterBitrate::Normal)), Some(CommonBitrateEnum::Normal)); + } + + #[test] + fn high_maps_to_common_high() { + assert_eq!(plan_bitrate(Some(RouterBitrate::High)), Some(CommonBitrateEnum::High)); + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0/build.rs index 24d87239d6..2a1a800991 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0/build.rs @@ -24,6 +24,8 @@ pub fn build_artcraft_seedance_2p0(builder: GenerateVideoRequestBuilder) -> Resu mod tests { use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum; + use crate::api::router_bitrate::RouterBitrate; use tokens::tokens::characters::CharacterToken; use tokens::tokens::media_files::MediaFileToken; @@ -72,6 +74,12 @@ mod tests { let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); })); assert_eq!(req.request.video_batch_count, Some(4)); } + + #[test] + fn bitrate_passed_through() { + let req = unwrap_request(make_builder(|b| { b.bitrate = Some(RouterBitrate::High); })); + assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High)); + } } // ── Resolution ── diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp/build.rs index 6979d85b76..1e395222c4 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp/build.rs @@ -24,6 +24,8 @@ pub fn build_artcraft_seedance_2p0_bp(builder: GenerateVideoRequestBuilder) -> R mod tests { use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum; + use crate::api::router_bitrate::RouterBitrate; use tokens::tokens::characters::CharacterToken; use tokens::tokens::media_files::MediaFileToken; @@ -70,6 +72,12 @@ mod tests { let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); })); assert_eq!(req.request.video_batch_count, Some(4)); } + + #[test] + fn bitrate_passed_through() { + let req = unwrap_request(make_builder(|b| { b.bitrate = Some(RouterBitrate::High); })); + assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High)); + } } mod resolution_tests { diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp_fast/build.rs index b998762ef0..4b9eebf3cc 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp_fast/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp_fast/build.rs @@ -24,6 +24,8 @@ pub fn build_artcraft_seedance_2p0_bp_fast(builder: GenerateVideoRequestBuilder) mod tests { use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum; + use crate::api::router_bitrate::RouterBitrate; use tokens::tokens::characters::CharacterToken; use tokens::tokens::media_files::MediaFileToken; @@ -72,6 +74,12 @@ mod tests { let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); })); assert_eq!(req.request.video_batch_count, Some(4)); } + + #[test] + fn bitrate_passed_through() { + let req = unwrap_request(make_builder(|b| { b.bitrate = Some(RouterBitrate::High); })); + assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High)); + } } mod resolution_tests { diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu/build.rs index 7e21211117..6aa1fc2724 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu/build.rs @@ -24,6 +24,8 @@ pub fn build_artcraft_seedance_2p0_bpu(builder: GenerateVideoRequestBuilder) -> mod tests { use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum; + use crate::api::router_bitrate::RouterBitrate; use tokens::tokens::characters::CharacterToken; use tokens::tokens::media_files::MediaFileToken; @@ -70,6 +72,12 @@ mod tests { let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); })); assert_eq!(req.request.video_batch_count, Some(4)); } + + #[test] + fn bitrate_passed_through() { + let req = unwrap_request(make_builder(|b| { b.bitrate = Some(RouterBitrate::High); })); + assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High)); + } } mod resolution_tests { diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu_fast/build.rs index 3519128bb2..5bf890b4ee 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu_fast/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu_fast/build.rs @@ -24,6 +24,8 @@ pub fn build_artcraft_seedance_2p0_bpu_fast(builder: GenerateVideoRequestBuilder mod tests { use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum; + use crate::api::router_bitrate::RouterBitrate; use tokens::tokens::characters::CharacterToken; use tokens::tokens::media_files::MediaFileToken; @@ -72,6 +74,12 @@ mod tests { let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); })); assert_eq!(req.request.video_batch_count, Some(4)); } + + #[test] + fn bitrate_passed_through() { + let req = unwrap_request(make_builder(|b| { b.bitrate = Some(RouterBitrate::High); })); + assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High)); + } } mod resolution_tests { diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_fast/build.rs index fff7972e38..cd9bbba91d 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_fast/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_fast/build.rs @@ -25,6 +25,8 @@ mod tests { use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum; use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum; + use crate::api::router_bitrate::RouterBitrate; use tokens::tokens::characters::CharacterToken; use tokens::tokens::media_files::MediaFileToken; @@ -83,6 +85,12 @@ mod tests { let req = unwrap_request(artcraft_fast_builder_with(|b| { b.video_batch_count = Some(4); })); assert_eq!(req.request.video_batch_count, Some(4)); } + + #[test] + fn bitrate_passed_through() { + let req = unwrap_request(artcraft_fast_builder_with(|b| { b.bitrate = Some(RouterBitrate::High); })); + assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High)); + } } // ── Aspect ratio ── diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u/build.rs index 83e87126a1..7d93b672f8 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u/build.rs @@ -24,6 +24,8 @@ pub fn build_artcraft_seedance_2p0_u(builder: GenerateVideoRequestBuilder) -> Re mod tests { use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum; + use crate::api::router_bitrate::RouterBitrate; use tokens::tokens::characters::CharacterToken; use tokens::tokens::media_files::MediaFileToken; @@ -72,6 +74,12 @@ mod tests { let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); })); assert_eq!(req.request.video_batch_count, Some(4)); } + + #[test] + fn bitrate_passed_through() { + let req = unwrap_request(make_builder(|b| { b.bitrate = Some(RouterBitrate::High); })); + assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High)); + } } // ── Resolution ── diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u_fast/build.rs index 00f665a691..b8f85eb71c 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u_fast/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u_fast/build.rs @@ -25,6 +25,8 @@ mod tests { use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum; use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum; + use crate::api::router_bitrate::RouterBitrate; use tokens::tokens::characters::CharacterToken; use tokens::tokens::media_files::MediaFileToken; @@ -83,6 +85,12 @@ mod tests { let req = unwrap_request(artcraft_fast_builder_with(|b| { b.video_batch_count = Some(4); })); assert_eq!(req.request.video_batch_count, Some(4)); } + + #[test] + fn bitrate_passed_through() { + let req = unwrap_request(artcraft_fast_builder_with(|b| { b.bitrate = Some(RouterBitrate::High); })); + assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High)); + } } // ── Aspect ratio ── diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/build.rs index 07e1fa3882..c14ea23a33 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/build.rs @@ -1,10 +1,12 @@ use seedance2pro_client::generate::video::generate_seedance_2p0::{ KinoviSeedance2p0AspectRatio as KinoviAspectRatio, + KinoviSeedance2p0Bitrate as KinoviBitrate, KinoviSeedance2p0BatchCount as KinoviBatchCount, KinoviSeedance2p0OutputResolution as KinoviOutputResolution, }; use crate::api::router_aspect_ratio::RouterAspectRatio; +use crate::api::router_bitrate::RouterBitrate; use crate::api::router_resolution::RouterResolution; use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; use crate::errors::artcraft_router_error::ArtcraftRouterError; @@ -26,6 +28,7 @@ fn do_build_kinovi_seedance_2p0(mut builder: GenerateVideoRequestBuilder) -> Res let resolution = plan_output_resolution(builder.resolution.take(), strategy)?; let batch_count = plan_batch_count(builder.video_batch_count.take(), strategy)?; let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?; + let bitrate = plan_bitrate(builder.bitrate.take()); let prompt = builder.prompt.take().unwrap_or_default(); let unhandled_request_state = KinoviSeedance2p0RemainingItems { @@ -42,6 +45,7 @@ fn do_build_kinovi_seedance_2p0(mut builder: GenerateVideoRequestBuilder) -> Res resolution, batch_count, duration_seconds, + bitrate, prompt, unhandled_request_state: Some(unhandled_request_state), }) @@ -195,6 +199,16 @@ fn plan_duration( } } +// Seedance 2.0 Pro bitrate: "standard" (default, field omitted) or "high". +// Bitrate does not affect cost. `Normal` and an unset value both map to the +// standard bitrate (None); only `High` requests the higher bitrate. +fn plan_bitrate(bitrate: Option) -> Option { + match bitrate { + Some(RouterBitrate::High) => Some(KinoviBitrate::High), + Some(RouterBitrate::Normal) | None => None, + } +} + #[cfg(test)] mod tests { use seedance2pro_client::generate::video::generate_seedance_2p0::{ @@ -391,6 +405,39 @@ mod tests { } } + // ── Bitrate conversions ── + + mod bitrate_conversions { + use super::*; + + #[test] + fn bitrate_high() { + let builder = GenerateVideoRequestBuilder { + bitrate: Some(RouterBitrate::High), + ..seedance2pro_builder() + }; + let draft = unwrap_draft(build_kinovi_seedance_2p0(builder)); + assert!(matches!(draft.bitrate, Some(KinoviBitrate::High))); + } + + #[test] + fn bitrate_normal_maps_to_standard() { + let builder = GenerateVideoRequestBuilder { + bitrate: Some(RouterBitrate::Normal), + ..seedance2pro_builder() + }; + let draft = unwrap_draft(build_kinovi_seedance_2p0(builder)); + assert!(draft.bitrate.is_none()); + } + + #[test] + fn bitrate_none_maps_to_standard() { + let builder = GenerateVideoRequestBuilder { bitrate: None, ..seedance2pro_builder() }; + let draft = unwrap_draft(build_kinovi_seedance_2p0(builder)); + assert!(draft.bitrate.is_none()); + } + } + // ── unhandled_request_state: media refs are placed there ── mod unhandled_request_state { diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/cost.rs index 94e66049dd..bd2c15db63 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/cost.rs @@ -63,6 +63,7 @@ impl KinoviSeedance2p0CostState { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }; let costs = pricing_request.calculate_costs(); @@ -476,6 +477,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/draft.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/draft.rs index 987dcc7a8c..91b6888abb 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/draft.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/draft.rs @@ -1,5 +1,5 @@ use seedance2pro_client::generate::video::generate_seedance_2p0::{ - GenerateSeedance2p0Request, KinoviSeedance2p0AspectRatio, + GenerateSeedance2p0Request, KinoviSeedance2p0AspectRatio, KinoviSeedance2p0Bitrate, KinoviSeedance2p0BatchCount, KinoviSeedance2p0OutputResolution, }; @@ -26,6 +26,7 @@ pub struct KinoviSeedance2p0DraftState { pub resolution: Option, pub duration_seconds: u8, pub batch_count: KinoviSeedance2p0BatchCount, + pub bitrate: Option, // Pending types that need to be queried. pub unhandled_request_state: Option, @@ -93,6 +94,7 @@ impl KinoviSeedance2p0DraftState { reference_audio_urls, character_ids, use_face_blur_hack: None, + bitrate: self.bitrate, }; Ok(KinoviSeedance2p0RequestState { request }) diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/build.rs index cc23e6c4af..03133005c0 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/build.rs @@ -1,10 +1,12 @@ use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{ KinoviSeedance2p0FastAspectRatio as KinoviAspectRatio, + KinoviSeedance2p0FastBitrate as KinoviBitrate, KinoviSeedance2p0FastBatchCount as KinoviBatchCount, KinoviSeedance2p0FastOutputResolution as KinoviOutputResolution, }; use crate::api::router_aspect_ratio::RouterAspectRatio; +use crate::api::router_bitrate::RouterBitrate; use crate::api::router_resolution::RouterResolution; use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; use crate::errors::artcraft_router_error::ArtcraftRouterError; @@ -26,6 +28,7 @@ fn do_build_kinovi_seedance_2p0_fast(mut builder: GenerateVideoRequestBuilder) - let resolution = plan_output_resolution(builder.resolution.take(), strategy)?; let batch_count = plan_batch_count(builder.video_batch_count.take(), strategy)?; let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?; + let bitrate = plan_bitrate(builder.bitrate.take()); let prompt = builder.prompt.take().unwrap_or_default(); let unhandled_request_state = KinoviSeedance2p0FastRemainingItems { @@ -42,6 +45,7 @@ fn do_build_kinovi_seedance_2p0_fast(mut builder: GenerateVideoRequestBuilder) - resolution, batch_count, duration_seconds, + bitrate, prompt, unhandled_request_state: Some(unhandled_request_state), }) @@ -210,6 +214,16 @@ fn plan_duration( } } +// Seedance 2.0 Fast bitrate: "standard" (default, field omitted) or "high". +// Bitrate does not affect cost. `Normal` and an unset value both map to the +// standard bitrate (None); only `High` requests the higher bitrate. +fn plan_bitrate(bitrate: Option) -> Option { + match bitrate { + Some(RouterBitrate::High) => Some(KinoviBitrate::High), + Some(RouterBitrate::Normal) | None => None, + } +} + #[cfg(test)] mod tests { use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{ @@ -430,6 +444,39 @@ mod tests { } } + // ── Bitrate conversions ── + + mod bitrate_conversions { + use super::*; + + #[test] + fn bitrate_high() { + let builder = GenerateVideoRequestBuilder { + bitrate: Some(RouterBitrate::High), + ..seedance2pro_fast_builder() + }; + let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder)); + assert!(matches!(draft.bitrate, Some(KinoviBitrate::High))); + } + + #[test] + fn bitrate_normal_maps_to_standard() { + let builder = GenerateVideoRequestBuilder { + bitrate: Some(RouterBitrate::Normal), + ..seedance2pro_fast_builder() + }; + let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder)); + assert!(draft.bitrate.is_none()); + } + + #[test] + fn bitrate_none_maps_to_standard() { + let builder = GenerateVideoRequestBuilder { bitrate: None, ..seedance2pro_fast_builder() }; + let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder)); + assert!(draft.bitrate.is_none()); + } + } + // ── unhandled_request_state: media refs are placed there ── mod unhandled_request_state { diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/cost.rs index 10b616839c..10bb6beeaf 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/cost.rs @@ -63,6 +63,7 @@ impl KinoviSeedance2p0FastCostState { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }; let costs = pricing_request.calculate_costs(); @@ -428,6 +429,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/draft.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/draft.rs index 00520b8e8d..988feb4c40 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/draft.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/draft.rs @@ -1,5 +1,5 @@ use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{ - GenerateSeedance2p0FastRequest, KinoviSeedance2p0FastAspectRatio, + GenerateSeedance2p0FastRequest, KinoviSeedance2p0FastAspectRatio, KinoviSeedance2p0FastBitrate, KinoviSeedance2p0FastBatchCount, KinoviSeedance2p0FastOutputResolution, }; @@ -24,6 +24,7 @@ pub struct KinoviSeedance2p0FastDraftState { pub resolution: Option, pub duration_seconds: u8, pub batch_count: KinoviSeedance2p0FastBatchCount, + pub bitrate: Option, pub unhandled_request_state: Option, } @@ -90,6 +91,7 @@ impl KinoviSeedance2p0FastDraftState { reference_audio_urls, character_ids, use_face_blur_hack: None, + bitrate: self.bitrate, }; Ok(KinoviSeedance2p0FastRequestState { request }) diff --git a/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs b/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs index e72a9b2c3e..0ddfa1a485 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs @@ -351,6 +351,7 @@ pub fn base_video_request() -> GenerateVideoRequestBuilder { reference_character_tokens: None, resolution: None, aspect_ratio: None, + bitrate: None, duration_seconds: None, video_batch_count: None, generate_audio: None, diff --git a/crates/api_clients/seedance2pro_client/src/generate/video/generate_happy_horse_1p0.rs b/crates/api_clients/seedance2pro_client/src/generate/video/generate_happy_horse_1p0.rs index 69895cea02..9e019ac2fd 100644 --- a/crates/api_clients/seedance2pro_client/src/generate/video/generate_happy_horse_1p0.rs +++ b/crates/api_clients/seedance2pro_client/src/generate/video/generate_happy_horse_1p0.rs @@ -129,6 +129,7 @@ pub async fn generate_happy_horse_1p0( reference_audio_urls: None, character_ids: None, use_face_blur_hack: Some(false), + bitrate: None, }; let raw_response = workflow_run_task(WorkflowRunTaskArgs { diff --git a/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0.rs b/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0.rs index 8a846e0168..f3f4046194 100644 --- a/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0.rs +++ b/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0.rs @@ -3,7 +3,7 @@ use crate::error::seedance2pro_error::Seedance2ProError; use crate::cost::kinovi_seedance_generation_cost::KinoviSeedanceGenerationCost; use crate::requests::kinovi_host::KinoviHost; use crate::requests::workflow_run_task::workflow_run_task::{ - workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw, + workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw, KinoviBitrateRaw, KinoviModelTypeRaw, KinoviOutputResolutionRaw, WorkflowRunTaskArgs, WorkflowRunTaskRequest, }; @@ -32,6 +32,9 @@ pub struct GenerateSeedance2p0Request { pub reference_audio_urls: Option>, pub character_ids: Option>, pub use_face_blur_hack: Option, + /// Output video bitrate. None defaults to "standard"; `High` requests a + /// higher bitrate. Does not affect cost. + pub bitrate: Option, } // ── Enums ── @@ -60,6 +63,11 @@ pub enum KinoviSeedance2p0BatchCount { Four, } +#[derive(Debug, Clone, Copy)] +pub enum KinoviSeedance2p0Bitrate { + High, +} + // ── Pricing ── // // Seedance 2.0 Pro credit pricing: @@ -178,6 +186,7 @@ pub async fn generate_seedance_2p0( reference_audio_urls: req.reference_audio_urls, character_ids: req.character_ids, use_face_blur_hack: req.use_face_blur_hack, + bitrate: map_bitrate(req.bitrate), }; let raw_response = workflow_run_task(WorkflowRunTaskArgs { @@ -224,6 +233,13 @@ fn map_batch_count(bc: Option) -> KinoviBatchCountR } } +fn map_bitrate(bitrate: Option) -> Option { + match bitrate { + Some(KinoviSeedance2p0Bitrate::High) => Some(KinoviBitrateRaw::High), + None => None, + } +} + // ── Tests ── #[cfg(test)] @@ -256,6 +272,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, } } @@ -810,6 +827,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }; assert_eq!( req.calculate_costs().total_cost.kinovi_credits, baseline, @@ -817,6 +835,21 @@ mod tests { ); } } + + // ── Bitrate doesn't affect cost ── + + #[test] + fn high_bitrate_does_not_affect_credits() { + let baseline = r720(5).calculate_costs().total_cost.kinovi_credits; + + let mut high = r720(5); + high.bitrate = Some(KinoviSeedance2p0Bitrate::High); + + assert_eq!( + high.calculate_costs().total_cost.kinovi_credits, baseline, + "High bitrate should not change credits from baseline {}", baseline, + ); + } } use crate::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs}; @@ -849,6 +882,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("t2v default — task_id={}, order_id={}", result.task_id, result.order_id); @@ -879,6 +913,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("t2v 1080p — task_id={}, order_id={}", result.task_id, result.order_id); @@ -912,6 +947,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("t2v 21:9 — task_id={}, order_id={}", result.task_id, result.order_id); @@ -943,6 +979,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("keyframe 21:9 — task_id={}, order_id={}", result.task_id, result.order_id); @@ -978,6 +1015,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("keyframe — task_id={}, order_id={}", result.task_id, result.order_id); @@ -1010,6 +1048,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("keyframe start+end — task_id={}, order_id={}", result.task_id, result.order_id); @@ -1046,6 +1085,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("image ref — task_id={}, order_id={}", result.task_id, result.order_id); @@ -1082,6 +1122,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("video ref — task_id={}, order_id={}", result.task_id, result.order_id); @@ -1116,6 +1157,7 @@ mod tests { reference_audio_urls: None, character_ids: Some(vec![STEAMPUNK_CLOWN_ID.to_string()]), use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("character ref — task_id={}, order_id={}", result.task_id, result.order_id); @@ -1146,6 +1188,7 @@ mod tests { reference_audio_urls: None, character_ids: Some(vec![STEAMPUNK_CLOWN_ID.to_string(), MOCHI_ID.to_string()]), use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("two characters — task_id={}, order_id={}", result.task_id, result.order_id); diff --git a/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0_fast.rs b/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0_fast.rs index b12d9b05cd..8d3881f36b 100644 --- a/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0_fast.rs +++ b/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0_fast.rs @@ -3,7 +3,7 @@ use crate::error::seedance2pro_error::Seedance2ProError; use crate::cost::kinovi_seedance_generation_cost::KinoviSeedanceGenerationCost; use crate::requests::kinovi_host::KinoviHost; use crate::requests::workflow_run_task::workflow_run_task::{ - workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw, + workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw, KinoviBitrateRaw, KinoviModelTypeRaw, KinoviOutputResolutionRaw, WorkflowRunTaskArgs, WorkflowRunTaskRequest, }; @@ -32,6 +32,9 @@ pub struct GenerateSeedance2p0FastRequest { pub reference_audio_urls: Option>, pub character_ids: Option>, pub use_face_blur_hack: Option, + /// Output video bitrate. None defaults to "standard"; `High` requests a + /// higher bitrate. Does not affect cost. + pub bitrate: Option, } // ── Enums ── @@ -59,6 +62,11 @@ pub enum KinoviSeedance2p0FastBatchCount { Four, } +#[derive(Debug, Clone, Copy)] +pub enum KinoviSeedance2p0FastBitrate { + High, +} + // ── Pricing ── // // Seedance 2.0 Fast credit pricing: @@ -173,6 +181,7 @@ pub async fn generate_seedance_2p0_fast( reference_audio_urls: req.reference_audio_urls, character_ids: req.character_ids, use_face_blur_hack: req.use_face_blur_hack, + bitrate: map_bitrate(req.bitrate), }; let raw_response = workflow_run_task(WorkflowRunTaskArgs { @@ -218,6 +227,13 @@ fn map_batch_count(bc: Option) -> KinoviBatchCo } } +fn map_bitrate(bitrate: Option) -> Option { + match bitrate { + Some(KinoviSeedance2p0FastBitrate::High) => Some(KinoviBitrateRaw::High), + None => None, + } +} + // ── Tests ── #[cfg(test)] @@ -250,6 +266,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, } } @@ -700,6 +717,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }; assert_eq!( req.calculate_costs().total_cost.kinovi_credits, baseline, @@ -707,6 +725,21 @@ mod tests { ); } } + + // ── Bitrate doesn't affect cost ── + + #[test] + fn high_bitrate_does_not_affect_credits() { + let baseline = r720(5).calculate_costs().total_cost.kinovi_credits; + + let mut high = r720(5); + high.bitrate = Some(KinoviSeedance2p0FastBitrate::High); + + assert_eq!( + high.calculate_costs().total_cost.kinovi_credits, baseline, + "High bitrate should not change credits from baseline {}", baseline, + ); + } } use crate::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs}; @@ -739,6 +772,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("t2v fast default — task_id={}, order_id={}", result.task_id, result.order_id); @@ -769,6 +803,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("t2v fast 480p — task_id={}, order_id={}", result.task_id, result.order_id); @@ -802,6 +837,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("fast t2v 21:9 480p — task_id={}, order_id={}", result.task_id, result.order_id); @@ -833,6 +869,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("fast keyframe 21:9 — task_id={}, order_id={}", result.task_id, result.order_id); @@ -868,6 +905,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("fast keyframe — task_id={}, order_id={}", result.task_id, result.order_id); @@ -904,6 +942,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("fast image ref — task_id={}, order_id={}", result.task_id, result.order_id); @@ -940,6 +979,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("fast video ref — task_id={}, order_id={}", result.task_id, result.order_id); @@ -974,6 +1014,7 @@ mod tests { reference_audio_urls: None, character_ids: Some(vec![MOCHI_ID.to_string()]), use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("fast character — task_id={}, order_id={}", result.task_id, result.order_id); @@ -1004,6 +1045,7 @@ mod tests { reference_audio_urls: None, character_ids: Some(vec![STEAMPUNK_CLOWN_ID.to_string(), MOCHI_ID.to_string()]), use_face_blur_hack: None, + bitrate: None, }, }).await?; println!("fast two characters — task_id={}, order_id={}", result.task_id, result.order_id); diff --git a/crates/api_clients/seedance2pro_client/src/requests/workflow_run_task/request_types.rs b/crates/api_clients/seedance2pro_client/src/requests/workflow_run_task/request_types.rs index 3ae8a16c55..e2965d86a0 100644 --- a/crates/api_clients/seedance2pro_client/src/requests/workflow_run_task/request_types.rs +++ b/crates/api_clients/seedance2pro_client/src/requests/workflow_run_task/request_types.rs @@ -40,6 +40,8 @@ pub (super) struct ApiParams { pub audio_urls: Option>, #[serde(rename = "batchCount", skip_serializing_if = "Option::is_none")] pub batch_count: Option, + #[serde(rename = "bitrate_mode", skip_serializing_if = "Option::is_none")] + pub bitrate_mode: Option<&'static str>, } #[derive(Deserialize, Debug)] diff --git a/crates/api_clients/seedance2pro_client/src/requests/workflow_run_task/workflow_run_task.rs b/crates/api_clients/seedance2pro_client/src/requests/workflow_run_task/workflow_run_task.rs index a481a197c4..0747ce5456 100644 --- a/crates/api_clients/seedance2pro_client/src/requests/workflow_run_task/workflow_run_task.rs +++ b/crates/api_clients/seedance2pro_client/src/requests/workflow_run_task/workflow_run_task.rs @@ -69,6 +69,10 @@ pub struct WorkflowRunTaskRequest { /// Controls the `faceBlurMode` field: true sends "on", false sends "off", None omits it. pub use_face_blur_hack: Option, + + /// Output video bitrate. None defaults to "standard" (the field is omitted); + /// `High` sends `bitrate_mode: "high"`. Does not affect cost. + pub bitrate: Option, } impl std::fmt::Debug for WorkflowRunTaskRequest { @@ -87,6 +91,7 @@ impl std::fmt::Debug for WorkflowRunTaskRequest { .field("character_ids", &self.character_ids) .field("output_resolution", &self.output_resolution) .field("use_face_blur_hack", &self.use_face_blur_hack) + .field("bitrate", &self.bitrate) .finish() } } @@ -268,6 +273,23 @@ impl KinoviModelTypeRaw { } } +/// Output video bitrate. When omitted, defaults to "standard". +#[derive(Debug, Clone, Copy)] +pub enum KinoviBitrateRaw { + /// High bitrate (`bitrate_mode: "high"`). + High, +} + +impl KinoviBitrateRaw { + /// Returns the API string to send, or None for "standard" (the default, + /// which is expressed by omitting the field entirely). + pub fn as_api_str(&self) -> Option<&'static str> { + match self { + Self::High => Some("high"), + } + } +} + // --- Response --- pub struct WorkflowRunTaskResponse { @@ -361,6 +383,7 @@ pub async fn workflow_run_task(args: WorkflowRunTaskArgs<'_>) -> Result) -> ApiParams { + ApiParams { + prompt: "a corgi".to_string(), + resolution: "1280x720".to_string(), + content_mode: "normal", + model: "seedance-20", + duration: "5s".to_string(), + mode: "keyframe", + output_resolution: None, + face_blur_mode: None, + character_ids: None, + uploaded_urls: None, + audio_urls: None, + batch_count: None, + bitrate_mode, + } + } + } + mod real_requests { use super::*; @@ -767,6 +835,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -800,6 +869,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -835,6 +905,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -869,6 +940,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -905,6 +977,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -958,6 +1031,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -1010,6 +1084,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -1062,6 +1137,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -1123,6 +1199,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -1177,6 +1254,7 @@ mod tests { reference_audio_urls: Some(vec![upload_result.public_url]), character_ids: None, use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -1216,6 +1294,7 @@ mod tests { reference_audio_urls: None, character_ids: Some(vec![STEAMPUNK_CLOWN_ID.to_string()]), use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -1249,6 +1328,7 @@ mod tests { reference_audio_urls: None, character_ids: Some(vec![MOCHI_ID.to_string()]), use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -1284,6 +1364,7 @@ mod tests { reference_audio_urls: None, character_ids: Some(vec![STEAMPUNK_CLOWN_ID.to_string()]), use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -1320,6 +1401,7 @@ mod tests { MOCHI_ID.to_string(), ]), use_face_blur_hack: None, + bitrate: None, output_resolution: None, }, }; @@ -1357,6 +1439,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: Some(false), + bitrate: None, output_resolution: Some(KinoviOutputResolutionRaw::TenEightyP), }, }; @@ -1408,6 +1491,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: Some(false), + bitrate: None, output_resolution: None, }, }; @@ -1459,6 +1543,7 @@ mod tests { reference_audio_urls: None, character_ids: None, use_face_blur_hack: Some(false), + bitrate: None, output_resolution: Some(KinoviOutputResolutionRaw::TenEightyP), }, }; @@ -1503,6 +1588,7 @@ mod tests { character_ids: None, output_resolution, use_face_blur_hack: None, + bitrate: None, }, } } diff --git a/crates/cli/production/support-tool/src/commands/artcraft/subcommands/generate_video.rs b/crates/cli/production/support-tool/src/commands/artcraft/subcommands/generate_video.rs index 4672666ac5..f543911959 100644 --- a/crates/cli/production/support-tool/src/commands/artcraft/subcommands/generate_video.rs +++ b/crates/cli/production/support-tool/src/commands/artcraft/subcommands/generate_video.rs @@ -119,6 +119,7 @@ pub async fn run(state: &ArtcraftState, args: GenerateVideoArgs) -> anyhow::Resu reference_character_tokens: None, resolution: None, aspect_ratio: None, + bitrate: None, quality: None, duration_seconds: Some(duration), video_batch_count: None, diff --git a/crates/cli/production/support-tool/src/commands/seedance2pro/subcommands/generate_video.rs b/crates/cli/production/support-tool/src/commands/seedance2pro/subcommands/generate_video.rs index d1dd60af2d..1fa2bee1b6 100644 --- a/crates/cli/production/support-tool/src/commands/seedance2pro/subcommands/generate_video.rs +++ b/crates/cli/production/support-tool/src/commands/seedance2pro/subcommands/generate_video.rs @@ -427,6 +427,7 @@ async fn generate( reference_audio_urls: media.reference_audio_urls, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, session, host_override: None, @@ -456,6 +457,7 @@ async fn generate( reference_audio_urls: media.reference_audio_urls, character_ids: None, use_face_blur_hack: None, + bitrate: None, }, session, host_override: None, diff --git a/crates/desktop/artcraft/src/core/commands/generate/common/router_video_request_to_artcraft_prompt.rs b/crates/desktop/artcraft/src/core/commands/generate/common/router_video_request_to_artcraft_prompt.rs index 1196095c29..bddb27002f 100644 --- a/crates/desktop/artcraft/src/core/commands/generate/common/router_video_request_to_artcraft_prompt.rs +++ b/crates/desktop/artcraft/src/core/commands/generate/common/router_video_request_to_artcraft_prompt.rs @@ -146,6 +146,7 @@ mod tests { reference_character_tokens: None, resolution: None, aspect_ratio: None, + bitrate: None, duration_seconds: None, video_batch_count: None, generate_audio: None, diff --git a/crates/desktop/artcraft/src/core/commands/generate/generate_video/artcraft/handle_artcraft_video_via_router.rs b/crates/desktop/artcraft/src/core/commands/generate/generate_video/artcraft/handle_artcraft_video_via_router.rs index 7f25442375..50924199a1 100644 --- a/crates/desktop/artcraft/src/core/commands/generate/generate_video/artcraft/handle_artcraft_video_via_router.rs +++ b/crates/desktop/artcraft/src/core/commands/generate/generate_video/artcraft/handle_artcraft_video_via_router.rs @@ -54,6 +54,7 @@ pub(super) async fn handle_artcraft_video_via_router( reference_character_tokens, resolution: request.resolution, aspect_ratio: request.aspect_ratio, + bitrate: None, duration_seconds: request.duration_seconds, video_batch_count: request.video_batch_count, generate_audio: request.generate_audio, diff --git a/crates/schema/public/enums/src/common/generation/common_bitrate.rs b/crates/schema/public/enums/src/common/generation/common_bitrate.rs new file mode 100644 index 0000000000..3dde645ffe --- /dev/null +++ b/crates/schema/public/enums/src/common/generation/common_bitrate.rs @@ -0,0 +1,162 @@ +use std::collections::BTreeSet; + +#[cfg(test)] +use strum::EnumCount; +#[cfg(test)] +use strum::EnumIter; +use utoipa::ToSchema; + +use crate::error::enum_error::EnumError; + +/// Maximum serialized string length for database storage. +pub const MAX_LENGTH: usize = 16; + +/// Common output bitrate levels for generation. +/// +/// Most models default to `Normal`; `High` requests a higher bitrate where the +/// model supports it. Models that don't support it simply ignore the value. +/// +/// NB: Keep the max serialized length to 16 characters. +#[cfg_attr(test, derive(EnumIter, EnumCount))] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum CommonBitrate { + Normal, + High, +} + +impl_enum_display_and_debug_using_to_str!(CommonBitrate); +impl_mysql_enum_coders!(CommonBitrate); +impl_mysql_from_row!(CommonBitrate); + +impl CommonBitrate { + pub fn to_str(&self) -> &'static str { + match self { + Self::Normal => "normal", + Self::High => "high", + } + } + + pub fn from_str(value: &str) -> Result { + match value { + "normal" => Ok(Self::Normal), + "high" => Ok(Self::High), + _ => Err(EnumError::CouldNotConvertFromString(value.to_string())), + } + } + + pub fn all_variants() -> BTreeSet { + BTreeSet::from([ + Self::Normal, + Self::High, + ]) + } +} + +#[cfg(test)] +mod tests { + use crate::common::generation::common_bitrate::CommonBitrate; + use crate::common::generation::common_bitrate::MAX_LENGTH; + use crate::test_helpers::assert_serialization; + + mod explicit_checks { + use super::*; + use crate::error::enum_error::EnumError; + + #[test] + fn test_serialization() { + assert_serialization(CommonBitrate::Normal, "normal"); + assert_serialization(CommonBitrate::High, "high"); + } + + #[test] + fn to_str() { + assert_eq!(CommonBitrate::Normal.to_str(), "normal"); + assert_eq!(CommonBitrate::High.to_str(), "high"); + } + + #[test] + fn from_str() { + assert_eq!(CommonBitrate::from_str("normal").unwrap(), CommonBitrate::Normal); + assert_eq!(CommonBitrate::from_str("high").unwrap(), CommonBitrate::High); + } + + #[test] + fn from_str_err() { + let result = CommonBitrate::from_str("invalid"); + assert!(result.is_err()); + if let Err(EnumError::CouldNotConvertFromString(value)) = result { + assert_eq!(value, "invalid"); + } else { + panic!("Expected EnumError::CouldNotConvertFromString"); + } + } + + #[test] + fn all_variants() { + let mut variants = CommonBitrate::all_variants(); + assert_eq!(variants.len(), 2); + assert_eq!(variants.pop_first(), Some(CommonBitrate::Normal)); + assert_eq!(variants.pop_first(), Some(CommonBitrate::High)); + assert_eq!(variants.pop_first(), None); + } + } + + mod mechanical_checks { + use super::*; + + #[test] + fn variant_length() { + use strum::IntoEnumIterator; + assert_eq!(CommonBitrate::all_variants().len(), CommonBitrate::iter().len()); + } + + #[test] + fn round_trip() { + for variant in CommonBitrate::all_variants() { + assert_eq!(variant, CommonBitrate::from_str(variant.to_str()).unwrap()); + assert_eq!(variant, CommonBitrate::from_str(&format!("{}", variant)).unwrap()); + assert_eq!(variant, CommonBitrate::from_str(&format!("{:?}", variant)).unwrap()); + } + } + + #[test] + fn serialized_length_ok_for_database() { + for variant in CommonBitrate::all_variants() { + let serialized = variant.to_str(); + assert!(serialized.len() > 0, "variant {:?} is too short", variant); + assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long via to_str()", variant); + } + for variant in CommonBitrate::all_variants() { + let json = serde_json::to_string(&variant).unwrap().replace('"', ""); + assert!(json.len() <= MAX_LENGTH, "variant {:?} is too long via JSON: {:?}", variant, json); + } + } + + #[test] + fn serialized_names_must_not_contain_dots() { + for variant in CommonBitrate::all_variants() { + let to_str_value = variant.to_str(); + assert!(!to_str_value.contains('.'), "to_str() for {:?} contains a dot: {:?}", variant, to_str_value); + + let json_value = serde_json::to_string(&variant).unwrap().replace('"', ""); + assert!(!json_value.contains('.'), "JSON serialization for {:?} contains a dot: {:?}", variant, json_value); + } + } + + #[test] + fn serialized_names_must_only_contain_lowercase_alphanumeric_and_underscore() { + let valid_pattern = regex::Regex::new(r"^[a-z0-9_]+$").unwrap(); + + for variant in CommonBitrate::all_variants() { + let to_str_value = variant.to_str(); + assert!(valid_pattern.is_match(to_str_value), + "to_str() for {:?} contains invalid characters: {:?} (only a-z, 0-9, _ allowed)", variant, to_str_value); + + let json_value = serde_json::to_string(&variant).unwrap().replace('"', ""); + assert!(valid_pattern.is_match(&json_value), + "JSON serialization for {:?} contains invalid characters: {:?} (only a-z, 0-9, _ allowed)", variant, json_value); + } + } + } +} diff --git a/crates/schema/public/enums/src/common/generation/mod.rs b/crates/schema/public/enums/src/common/generation/mod.rs index 11128e7ac3..76f6b5890e 100644 --- a/crates/schema/public/enums/src/common/generation/mod.rs +++ b/crates/schema/public/enums/src/common/generation/mod.rs @@ -1,4 +1,5 @@ pub mod common_aspect_ratio; +pub mod common_bitrate; pub mod common_generation_mode; pub mod common_image_model; pub mod common_quality; diff --git a/crates/service/web/storyteller_web/src/configs/omni_gen/video_models.rs b/crates/service/web/storyteller_web/src/configs/omni_gen/video_models.rs index cb3dad22f8..c229c8d4f5 100644 --- a/crates/service/web/storyteller_web/src/configs/omni_gen/video_models.rs +++ b/crates/service/web/storyteller_web/src/configs/omni_gen/video_models.rs @@ -5,6 +5,7 @@ use enums::common::generation::common_video_model::CommonVideoModel; use enums::common::generation::model_creator::ModelCreator; use enums::common::generation_provider::GenerationProvider; use once_cell::sync::Lazy; +use enums::common::generation::common_bitrate::CommonBitrate; pub const OMNI_GEN_VIDEO_MODELS_AND_PROVIDERS: Lazy = Lazy::new(|| { let models = build_omni_gen_video_models(); @@ -246,6 +247,11 @@ fn build_omni_gen_video_models() -> Vec { CommonResolution::TenEightyP, ]), resolution_default: Some(CommonResolution::SevenTwentyP), + bitrate_options: Some(vec![ + CommonBitrate::Normal, + CommonBitrate::High, + ]), + bitrate_default: Some(CommonBitrate::Normal), batch_size_options: Some(vec![1, 2, 4]), batch_size_default: Some(1), duration_seconds_min: Some(4), @@ -287,6 +293,11 @@ fn build_omni_gen_video_models() -> Vec { CommonResolution::SevenTwentyP, ]), resolution_default: Some(CommonResolution::SevenTwentyP), + bitrate_options: Some(vec![ + CommonBitrate::Normal, + CommonBitrate::High, + ]), + bitrate_default: Some(CommonBitrate::Normal), batch_size_options: Some(vec![1, 2, 4]), batch_size_default: Some(1), duration_seconds_min: Some(4), @@ -329,6 +340,11 @@ fn build_omni_gen_video_models() -> Vec { CommonResolution::TenEightyP, ]), resolution_default: Some(CommonResolution::SevenTwentyP), + bitrate_options: Some(vec![ + CommonBitrate::Normal, + CommonBitrate::High, + ]), + bitrate_default: Some(CommonBitrate::Normal), batch_size_options: Some(vec![1, 2, 4]), batch_size_default: Some(1), duration_seconds_min: Some(4), @@ -370,6 +386,11 @@ fn build_omni_gen_video_models() -> Vec { CommonResolution::SevenTwentyP, ]), resolution_default: Some(CommonResolution::SevenTwentyP), + bitrate_options: Some(vec![ + CommonBitrate::Normal, + CommonBitrate::High, + ]), + bitrate_default: Some(CommonBitrate::Normal), batch_size_options: Some(vec![1, 2, 4]), batch_size_default: Some(1), duration_seconds_min: Some(4), @@ -412,6 +433,11 @@ fn build_omni_gen_video_models() -> Vec { CommonResolution::TenEightyP, ]), resolution_default: Some(CommonResolution::SevenTwentyP), + bitrate_options: Some(vec![ + CommonBitrate::Normal, + CommonBitrate::High, + ]), + bitrate_default: Some(CommonBitrate::Normal), batch_size_options: Some(vec![1, 2, 4]), batch_size_default: Some(1), duration_seconds_min: Some(4), @@ -453,6 +479,10 @@ fn build_omni_gen_video_models() -> Vec { CommonResolution::SevenTwentyP, ]), resolution_default: Some(CommonResolution::SevenTwentyP), + bitrate_options: Some(vec![ + CommonBitrate::Normal, + CommonBitrate::High, + ]), batch_size_options: Some(vec![1, 2, 4]), batch_size_default: Some(1), duration_seconds_min: Some(4), diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index fdd08f7799..ec0dd4caff 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -268,6 +268,7 @@ use enums::by_table::users::user_feature_flag::UserFeatureFlag; use enums::common::generation::common_model_class::CommonModelClass; use enums::common::generation::common_model_type::CommonModelType; use enums::common::generation::common_aspect_ratio::CommonAspectRatio; +use enums::common::generation::common_bitrate::CommonBitrate; use enums::common::generation::common_generation_mode::CommonGenerationMode; use enums::common::generation::common_resolution::CommonResolution; use enums::common::generation_provider::GenerationProvider; @@ -668,6 +669,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux CommonModelClass, CommonModelType, CommonAspectRatio, + CommonBitrate, CommonGenerationMode, CommonResolution, PromptContextSemanticType, 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 8a2964f0ec..0e56a68bb3 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 @@ -51,6 +51,7 @@ pub async fn estimate_video_cost_handler( reference_character_tokens: None, resolution: router_resolution, aspect_ratio: router_aspect_ratio, + bitrate: None, duration_seconds: request.duration_seconds, video_batch_count: request.video_batch_count, generate_audio: request.generate_audio, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/helpers/hydrate_router_request.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/helpers/hydrate_router_request.rs index 604697dc0e..0adac62f00 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/helpers/hydrate_router_request.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/helpers/hydrate_router_request.rs @@ -3,6 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use artcraft_router::api::audio_list_ref::AudioListRef; use artcraft_router::api::character_list_ref::CharacterListRef; use artcraft_router::api::router_aspect_ratio::RouterAspectRatio; +use artcraft_router::api::router_bitrate::RouterBitrate; use artcraft_router::api::router_resolution::RouterResolution; use artcraft_router::api::router_video_model::RouterVideoModel; use artcraft_router::api::image_list_ref::ImageListRef; @@ -12,6 +13,7 @@ use artcraft_router::api::video_list_ref::VideoListRef; use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; use artcraft_router::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum; +use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum; use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; @@ -36,6 +38,11 @@ pub fn hydrate_to_router_request( .map(convert_resolution) .transpose()?; + let bitrate = request.bitrate + .as_ref() + .map(convert_bitrate) + .transpose()?; + Ok(GenerateVideoRequestBuilder { model, provider: RouterProvider::Artcraft, @@ -55,6 +62,7 @@ pub fn hydrate_to_router_request( .map(CharacterListRef::CharacterTokens), resolution, aspect_ratio, + bitrate, duration_seconds: request.duration_seconds, video_batch_count: request.video_batch_count, generate_audio: request.generate_audio, @@ -95,3 +103,71 @@ fn convert_resolution( ) }) } + +fn convert_bitrate( + bitrate: &CommonBitrateEnum, +) -> Result { + let json = serde_json::to_string(bitrate)?; + serde_json::from_str(&json).map_err(|e| { + CommonWebError::BadInputWithSimpleMessage( + format!("Unsupported bitrate: {}", e), + ) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + mod bitrate_hydration { + use super::*; + + #[test] + fn high_is_hydrated() { + let request = OmniGenVideoCostAndGenerateRequest { + bitrate: Some(CommonBitrateEnum::High), + ..base_request() + }; + let builder = hydrate_to_router_request(&request).expect("hydrate should succeed"); + assert_eq!(builder.bitrate, Some(RouterBitrate::High)); + } + + #[test] + fn normal_is_hydrated() { + let request = OmniGenVideoCostAndGenerateRequest { + bitrate: Some(CommonBitrateEnum::Normal), + ..base_request() + }; + let builder = hydrate_to_router_request(&request).expect("hydrate should succeed"); + assert_eq!(builder.bitrate, Some(RouterBitrate::Normal)); + } + + #[test] + fn none_stays_none() { + let builder = hydrate_to_router_request(&base_request()).expect("hydrate should succeed"); + assert!(builder.bitrate.is_none()); + } + } + + fn base_request() -> OmniGenVideoCostAndGenerateRequest { + OmniGenVideoCostAndGenerateRequest { + idempotency_token: None, + model: Some(CommonVideoModelEnum::Seedance2p0), + prompt: None, + negative_prompt: None, + start_frame_image_media_token: None, + end_frame_image_media_token: None, + reference_image_media_tokens: None, + reference_video_media_tokens: None, + reference_audio_media_tokens: None, + reference_character_tokens: None, + resolution: None, + aspect_ratio: None, + bitrate: None, + quality: None, + duration_seconds: None, + video_batch_count: None, + generate_audio: None, + } + } +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/shared_utils/video/validate_when_image_required.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/shared_utils/video/validate_when_image_required.rs index c94faa6e48..3b6a84b709 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/shared_utils/video/validate_when_image_required.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/shared_utils/video/validate_when_image_required.rs @@ -56,6 +56,7 @@ mod tests { reference_character_tokens: None, resolution: None, aspect_ratio: None, + bitrate: None, quality: None, duration_seconds: Some(5), video_batch_count: Some(1), diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/helpers/hydrate_router_request.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/helpers/hydrate_router_request.rs index 604697dc0e..0adac62f00 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/helpers/hydrate_router_request.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/helpers/hydrate_router_request.rs @@ -3,6 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use artcraft_router::api::audio_list_ref::AudioListRef; use artcraft_router::api::character_list_ref::CharacterListRef; use artcraft_router::api::router_aspect_ratio::RouterAspectRatio; +use artcraft_router::api::router_bitrate::RouterBitrate; use artcraft_router::api::router_resolution::RouterResolution; use artcraft_router::api::router_video_model::RouterVideoModel; use artcraft_router::api::image_list_ref::ImageListRef; @@ -12,6 +13,7 @@ use artcraft_router::api::video_list_ref::VideoListRef; use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; use artcraft_router::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum; +use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum; use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; @@ -36,6 +38,11 @@ pub fn hydrate_to_router_request( .map(convert_resolution) .transpose()?; + let bitrate = request.bitrate + .as_ref() + .map(convert_bitrate) + .transpose()?; + Ok(GenerateVideoRequestBuilder { model, provider: RouterProvider::Artcraft, @@ -55,6 +62,7 @@ pub fn hydrate_to_router_request( .map(CharacterListRef::CharacterTokens), resolution, aspect_ratio, + bitrate, duration_seconds: request.duration_seconds, video_batch_count: request.video_batch_count, generate_audio: request.generate_audio, @@ -95,3 +103,71 @@ fn convert_resolution( ) }) } + +fn convert_bitrate( + bitrate: &CommonBitrateEnum, +) -> Result { + let json = serde_json::to_string(bitrate)?; + serde_json::from_str(&json).map_err(|e| { + CommonWebError::BadInputWithSimpleMessage( + format!("Unsupported bitrate: {}", e), + ) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + mod bitrate_hydration { + use super::*; + + #[test] + fn high_is_hydrated() { + let request = OmniGenVideoCostAndGenerateRequest { + bitrate: Some(CommonBitrateEnum::High), + ..base_request() + }; + let builder = hydrate_to_router_request(&request).expect("hydrate should succeed"); + assert_eq!(builder.bitrate, Some(RouterBitrate::High)); + } + + #[test] + fn normal_is_hydrated() { + let request = OmniGenVideoCostAndGenerateRequest { + bitrate: Some(CommonBitrateEnum::Normal), + ..base_request() + }; + let builder = hydrate_to_router_request(&request).expect("hydrate should succeed"); + assert_eq!(builder.bitrate, Some(RouterBitrate::Normal)); + } + + #[test] + fn none_stays_none() { + let builder = hydrate_to_router_request(&base_request()).expect("hydrate should succeed"); + assert!(builder.bitrate.is_none()); + } + } + + fn base_request() -> OmniGenVideoCostAndGenerateRequest { + OmniGenVideoCostAndGenerateRequest { + idempotency_token: None, + model: Some(CommonVideoModelEnum::Seedance2p0), + prompt: None, + negative_prompt: None, + start_frame_image_media_token: None, + end_frame_image_media_token: None, + reference_image_media_tokens: None, + reference_video_media_tokens: None, + reference_audio_media_tokens: None, + reference_character_tokens: None, + resolution: None, + aspect_ratio: None, + bitrate: None, + quality: None, + duration_seconds: None, + video_batch_count: None, + generate_audio: None, + } + } +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/shared_utils/video/validate_when_image_required.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/shared_utils/video/validate_when_image_required.rs index c94faa6e48..3b6a84b709 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/shared_utils/video/validate_when_image_required.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/shared_utils/video/validate_when_image_required.rs @@ -56,6 +56,7 @@ mod tests { reference_character_tokens: None, resolution: None, aspect_ratio: None, + bitrate: None, quality: None, duration_seconds: Some(5), video_batch_count: Some(1),