Compare commits

...
Author SHA1 Message Date
Brandon Thomas e67de36b8f annotate models with bitrate 2026-06-22 08:17:13 -04:00
Brandon Thomas 74ce0534ff test 2026-06-21 23:07:43 -04:00
Brandon Thomas 98e4e83f22 map type 2026-06-21 22:57:35 -04:00
Brandon Thomas 137acdfdc7 wiring 2026-06-21 22:45:33 -04:00
Brandon Thomas c0da33b1c4 add bitrate to kinovi artcraft_router 2026-06-21 22:34:39 -04:00
Brandon Thomas b202e7b351 add bitrate to router + omni 2026-06-21 21:52:41 -04:00
Brandon Thomas a2a6b699bb bitrate in seedance2pro client 2026-06-21 21:34:31 -04:00
39 changed files with 772 additions and 4 deletions
@@ -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<CommonAspectRatio>,
/// The output bitrate to use.
/// Not all models support this; models that don't simply ignore it.
pub bitrate: Option<CommonBitrate>,
/// The quality to use.
pub quality: Option<CommonQuality>,
@@ -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<CommonResolution>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bitrate_options: Option<Vec<CommonBitrate>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bitrate_default: Option<CommonBitrate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality_options: Option<Vec<CommonQuality>>,
@@ -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,
@@ -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;
@@ -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,
}
@@ -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<RouterAspectRatio>,
/// The output bitrate to use.
/// Not all models support this; models that don't simply ignore it.
pub bitrate: Option<RouterBitrate>,
/// How many seconds to generate.
pub duration_seconds: Option<u16>,
@@ -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,
@@ -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<RouterBitrate>) -> Option<CommonBitrateEnum> {
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<u16>,
@@ -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));
}
}
}
@@ -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 ──
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 ──
@@ -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 ──
@@ -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 ──
@@ -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<RouterBitrate>) -> Option<KinoviBitrate> {
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 {
@@ -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,
},
}
}
@@ -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<KinoviSeedance2p0OutputResolution>,
pub duration_seconds: u8,
pub batch_count: KinoviSeedance2p0BatchCount,
pub bitrate: Option<KinoviSeedance2p0Bitrate>,
// Pending types that need to be queried.
pub unhandled_request_state: Option<KinoviSeedance2p0RemainingItems>,
@@ -93,6 +94,7 @@ impl KinoviSeedance2p0DraftState {
reference_audio_urls,
character_ids,
use_face_blur_hack: None,
bitrate: self.bitrate,
};
Ok(KinoviSeedance2p0RequestState { request })
@@ -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<RouterBitrate>) -> Option<KinoviBitrate> {
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 {
@@ -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,
},
}
}
@@ -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<KinoviSeedance2p0FastOutputResolution>,
pub duration_seconds: u8,
pub batch_count: KinoviSeedance2p0FastBatchCount,
pub bitrate: Option<KinoviSeedance2p0FastBitrate>,
pub unhandled_request_state: Option<KinoviSeedance2p0FastRemainingItems>,
}
@@ -90,6 +91,7 @@ impl KinoviSeedance2p0FastDraftState {
reference_audio_urls,
character_ids,
use_face_blur_hack: None,
bitrate: self.bitrate,
};
Ok(KinoviSeedance2p0FastRequestState { request })
@@ -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,
@@ -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 {
@@ -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<Vec<String>>,
pub character_ids: Option<Vec<String>>,
pub use_face_blur_hack: Option<bool>,
/// Output video bitrate. None defaults to "standard"; `High` requests a
/// higher bitrate. Does not affect cost.
pub bitrate: Option<KinoviSeedance2p0Bitrate>,
}
// ── 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<KinoviSeedance2p0BatchCount>) -> KinoviBatchCountR
}
}
fn map_bitrate(bitrate: Option<KinoviSeedance2p0Bitrate>) -> Option<KinoviBitrateRaw> {
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);
@@ -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<Vec<String>>,
pub character_ids: Option<Vec<String>>,
pub use_face_blur_hack: Option<bool>,
/// Output video bitrate. None defaults to "standard"; `High` requests a
/// higher bitrate. Does not affect cost.
pub bitrate: Option<KinoviSeedance2p0FastBitrate>,
}
// ── 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<KinoviSeedance2p0FastBatchCount>) -> KinoviBatchCo
}
}
fn map_bitrate(bitrate: Option<KinoviSeedance2p0FastBitrate>) -> Option<KinoviBitrateRaw> {
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);
@@ -40,6 +40,8 @@ pub (super) struct ApiParams {
pub audio_urls: Option<Vec<String>>,
#[serde(rename = "batchCount", skip_serializing_if = "Option::is_none")]
pub batch_count: Option<u8>,
#[serde(rename = "bitrate_mode", skip_serializing_if = "Option::is_none")]
pub bitrate_mode: Option<&'static str>,
}
#[derive(Deserialize, Debug)]
@@ -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<bool>,
/// Output video bitrate. None defaults to "standard" (the field is omitted);
/// `High` sends `bitrate_mode: "high"`. Does not affect cost.
pub bitrate: Option<KinoviBitrateRaw>,
}
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<Workflow
uploaded_urls,
audio_urls,
batch_count,
bitrate_mode: req.bitrate.and_then(|bitrate| bitrate.as_api_str()),
},
},
},
@@ -470,6 +493,7 @@ mod tests {
character_ids: None,
output_resolution,
use_face_blur_hack: None,
bitrate: None,
}
}
@@ -635,6 +659,7 @@ mod tests {
character_ids: None,
output_resolution: None,
use_face_blur_hack: None,
bitrate: None,
};
assert_eq!(
req.estimate_credits(), baseline,
@@ -738,6 +763,49 @@ mod tests {
}
}
// ── Bitrate serialization ──
//
// The optional `bitrate_mode` field is sent only when `High` is requested;
// the standard (default) bitrate omits the field entirely.
mod bitrate_tests {
use super::*;
#[test]
fn high_bitrate_serializes_to_high() {
assert_eq!(KinoviBitrateRaw::High.as_api_str(), Some("high"));
let api_params = base_api_params(Some("high"));
let json = serde_json::to_string(&api_params).unwrap();
assert!(json.contains(r#""bitrate_mode":"high""#), "expected bitrate_mode in {json}");
}
#[test]
fn standard_bitrate_omits_field() {
let api_params = base_api_params(None);
let json = serde_json::to_string(&api_params).unwrap();
assert!(!json.contains("bitrate_mode"), "expected no bitrate_mode in {json}");
}
fn base_api_params(bitrate_mode: Option<&'static str>) -> 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,
},
}
}
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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<Self, EnumError> {
match value {
"normal" => Ok(Self::Normal),
"high" => Ok(Self::High),
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
}
}
pub fn all_variants() -> BTreeSet<Self> {
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);
}
}
}
}
@@ -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;
@@ -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<OmniGenVideoModelsResponse> = Lazy::new(|| {
let models = build_omni_gen_video_models();
@@ -246,6 +247,11 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
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<OmniGenVideoModelDetails> {
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<OmniGenVideoModelDetails> {
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<OmniGenVideoModelDetails> {
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<OmniGenVideoModelDetails> {
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<OmniGenVideoModelDetails> {
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),
@@ -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,
@@ -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,
@@ -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<RouterBitrate, CommonWebError> {
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,
}
}
}
@@ -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),
@@ -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<RouterBitrate, CommonWebError> {
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,
}
}
}
@@ -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),