Compare commits

..
Author SHA1 Message Date
Brandon Thomas d5a2b36525 promo pricing 2026-09-18 12:09:19 -04:00
Brandon Thomas e3c553ff2c wan 3.0 client bindings 2026-09-18 12:00:58 -04:00
Brandon Thomas 094d33e405 Merge pull request #21 from storytold/bt-batch-counts
standardize batch counts
2026-09-18 09:46:32 -04:00
Brandon Thomas 6a35fccba3 standardize / cleanup video batch sizes 2026-09-18 09:45:53 -04:00
Brandon Thomas db925450c5 video batching 2026-09-18 09:36:01 -04:00
Brandon Thomas 90f1fcb25f prices 2026-09-18 09:17:00 -04:00
Brandon Thomas 0245058e05 tests 2026-09-18 09:12:50 -04:00
Brandon Thomas 5d311b7a20 increase batch counts 2026-09-18 09:07:06 -04:00
Brandon Thomas 4836854263 Merge pull request #20 from storytold/bt-rate-increase
seedance rates
2026-09-17 12:12:07 -04:00
61 changed files with 3293 additions and 1209 deletions

No files matched your search

@@ -264,30 +264,6 @@ pub fn plan_batch_count(
}
}
/// Batch counts for the Seedance 2.0 Mini family: 1-8 are all valid
/// (wider than the shared 1/2/4 planning); higher counts clamp to 8 per the
/// mitigation strategy.
pub fn plan_mini_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<u16, ArtcraftRouterError> {
let count = video_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1..=8 => Ok(count),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "video_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(8),
},
}
}
/// Translate the router-facing bitrate into the API `CommonBitrate`.
///
/// Bitrate does not affect cost. An unset value leaves the field `None` (the
@@ -0,0 +1,259 @@
//! Public ArtCraft batch planning and retail price snapshots for Kinovi models.
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::router_provider::RouterProvider;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::video_list_ref::VideoListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
const MODELS: [RouterVideoModel; 12] = [
RouterVideoModel::Seedance2p5,
RouterVideoModel::Seedance2p0,
RouterVideoModel::Seedance2p0Fast,
RouterVideoModel::Seedance2p0Mini,
RouterVideoModel::Seedance2p5Ultra,
RouterVideoModel::Seedance2p0BytePlus,
RouterVideoModel::Seedance2p0BytePlusFast,
RouterVideoModel::Seedance2p0BytePlusMini,
RouterVideoModel::Seedance2p0BytePlusUltra,
RouterVideoModel::Seedance2p0BytePlusUltraFast,
RouterVideoModel::Seedance2p0BytePlusUltraMini,
RouterVideoModel::HappyHorse1p0,
];
mod batch_planning {
use super::*;
#[test]
fn batches_one_through_four_survive_http_serialization() {
for model in MODELS {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
for batch in [None, Some(1), Some(2), Some(3), Some(4)] {
let mut builder = builder(model, RouterResolution::SevenTwentyP, batch);
builder.request_mismatch_mitigation_strategy = strategy;
let request = http_request(builder.build2().unwrap());
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["video_batch_count"], batch.unwrap_or(1), "{model:?}");
let decoded: OmniGenVideoCostAndGenerateRequest = serde_json::from_value(json).unwrap();
assert_eq!(decoded.video_batch_count, Some(batch.unwrap_or(1)));
}
}
}
}
#[test]
fn zero_is_rejected_under_every_strategy() {
for model in MODELS {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let mut builder = builder(model, RouterResolution::SevenTwentyP, Some(0));
builder.request_mismatch_mitigation_strategy = strategy;
assert!(matches!(builder.build2(), Err(ArtcraftRouterError::Client(
ClientError::UserRequestedZeroGenerations
))), "{model:?}");
}
}
}
#[test]
fn batches_above_four_follow_the_mitigation_strategy() {
for model in MODELS {
for batch in [5, 6, 7, 8, u16::MAX] {
let builder = builder(model, RouterResolution::SevenTwentyP, Some(batch));
assert!(matches!(builder.clone().build2(), Err(ArtcraftRouterError::Client(
ClientError::ModelDoesNotSupportOption { field: "video_batch_count", .. }
))), "{model:?}, batch {batch}");
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let mut builder = builder.clone();
builder.request_mismatch_mitigation_strategy = strategy;
let request = http_request(builder.build2().unwrap());
assert_eq!(request.video_batch_count, Some(4), "{model:?}, batch {batch}");
}
}
}
}
}
mod retail_prices {
use super::*;
use RouterResolution::{FourEightyP, FourK, SevenTwentyP, TenEightyP};
use RouterVideoModel::{HappyHorse1p0, Seedance2p0, Seedance2p0Fast, Seedance2p0Mini, Seedance2p5,
Seedance2p5Ultra, Seedance2p0BytePlus, Seedance2p0BytePlusFast, Seedance2p0BytePlusMini, Seedance2p0BytePlusUltra, Seedance2p0BytePlusUltraFast, Seedance2p0BytePlusUltraMini,
};
#[test]
fn five_second_prices_for_every_resolution_and_batch() {
// Literal USD cents / ArtCraft credits for batches [1, 2, 3, 4].
// Pin the final rounded totals, not a multiple of an already-rounded price.
for (model, resolution, expected) in [
(Seedance2p5, FourEightyP, [59, 118, 177, 236]),
(Seedance2p5, SevenTwentyP, [134, 268, 401, 535]),
(Seedance2p5, TenEightyP, [309, 617, 925, 1233]),
(Seedance2p5Ultra, FourEightyP, [70, 140, 209, 279]),
(Seedance2p5Ultra, SevenTwentyP, [158, 316, 474, 632]),
(Seedance2p5Ultra, TenEightyP, [337, 673, 1009, 1345]),
(Seedance2p0, FourEightyP, [39, 78, 117, 155]),
(Seedance2p0, SevenTwentyP, [93, 185, 278, 370]),
(Seedance2p0, TenEightyP, [233, 466, 699, 933]),
(Seedance2p0, FourK, [463, 925, 1388, 1850]),
(Seedance2p0Fast, FourEightyP, [36, 71, 107, 143]),
(Seedance2p0Fast, SevenTwentyP, [64, 127, 191, 255]),
(Seedance2p0Mini, FourEightyP, [18, 35, 52, 69]),
(Seedance2p0Mini, SevenTwentyP, [45, 89, 134, 178]),
(Seedance2p0BytePlus, FourEightyP, [50, 100, 150, 200]),
(Seedance2p0BytePlus, SevenTwentyP, [125, 250, 375, 500]),
(Seedance2p0BytePlus, TenEightyP, [250, 500, 750, 1000]),
(Seedance2p0BytePlus, FourK, [475, 950, 1425, 1900]),
(Seedance2p0BytePlusUltra, FourEightyP, [50, 100, 150, 200]),
(Seedance2p0BytePlusUltra, SevenTwentyP, [125, 250, 375, 500]),
(Seedance2p0BytePlusUltra, TenEightyP, [250, 500, 750, 1000]),
(Seedance2p0BytePlusUltra, FourK, [475, 950, 1425, 1900]),
(Seedance2p0BytePlusFast, FourEightyP, [45, 90, 135, 180]),
(Seedance2p0BytePlusFast, SevenTwentyP, [100, 200, 300, 400]),
(Seedance2p0BytePlusUltraFast, FourEightyP, [45, 90, 135, 180]),
(Seedance2p0BytePlusUltraFast, SevenTwentyP, [100, 200, 300, 400]),
(Seedance2p0BytePlusMini, FourEightyP, [18, 36, 54, 71]),
(Seedance2p0BytePlusMini, SevenTwentyP, [46, 91, 137, 182]),
(Seedance2p0BytePlusUltraMini, FourEightyP, [18, 36, 54, 71]),
(Seedance2p0BytePlusUltraMini, SevenTwentyP, [46, 91, 137, 182]),
(HappyHorse1p0, SevenTwentyP, [85, 171, 256, 342]),
(HappyHorse1p0, TenEightyP, [171, 342, 513, 684]),
] {
assert_batch_prices(builder(model, resolution, None), expected);
}
}
#[test]
fn video_reference_prices_for_every_resolution_and_batch() {
// Five output seconds, plus ten measured input seconds for Seedance 2.5.
for (model, resolution, expected) in [
(Seedance2p5, FourEightyP, [109, 218, 326, 435]),
(Seedance2p5, SevenTwentyP, [238, 476, 713, 951]),
(Seedance2p5, TenEightyP, [578, 1155, 1733, 2310]),
(Seedance2p5Ultra, FourEightyP, [129, 257, 386, 514]),
(Seedance2p5Ultra, SevenTwentyP, [281, 562, 843, 1124]),
(Seedance2p5Ultra, TenEightyP, [628, 1256, 1884, 2511]),
(Seedance2p0, FourEightyP, [45, 89, 133, 177]),
(Seedance2p0, SevenTwentyP, [111, 221, 331, 441]),
(Seedance2p0, TenEightyP, [256, 511, 767, 1022]),
(Seedance2p0, FourK, [569, 1138, 1707, 2276]),
(Seedance2p0Fast, FourEightyP, [43, 85, 127, 169]),
(Seedance2p0Fast, SevenTwentyP, [78, 156, 234, 312]),
// Preserve Mini's existing floating-point round-up behavior.
(Seedance2p0Mini, FourEightyP, [22, 44, 66, 88]),
(Seedance2p0Mini, SevenTwentyP, [54, 108, 161, 215]),
(Seedance2p0BytePlus, FourEightyP, [51, 102, 153, 204]),
(Seedance2p0BytePlus, SevenTwentyP, [129, 257, 386, 514]),
(Seedance2p0BytePlus, TenEightyP, [289, 578, 867, 1156]),
(Seedance2p0BytePlus, FourK, [595, 1190, 1785, 2380]),
(Seedance2p0BytePlusUltra, FourEightyP, [51, 102, 153, 204]),
(Seedance2p0BytePlusUltra, SevenTwentyP, [129, 257, 386, 514]),
(Seedance2p0BytePlusUltra, TenEightyP, [289, 578, 867, 1156]),
(Seedance2p0BytePlusUltra, FourK, [595, 1190, 1785, 2380]),
(Seedance2p0BytePlusFast, FourEightyP, [49, 97, 145, 193]),
(Seedance2p0BytePlusFast, SevenTwentyP, [103, 205, 308, 410]),
(Seedance2p0BytePlusUltraFast, FourEightyP, [49, 97, 145, 193]),
(Seedance2p0BytePlusUltraFast, SevenTwentyP, [103, 205, 308, 410]),
(Seedance2p0BytePlusMini, FourEightyP, [23, 45, 68, 90]),
(Seedance2p0BytePlusMini, SevenTwentyP, [55, 110, 165, 219]),
(Seedance2p0BytePlusUltraMini, FourEightyP, [23, 45, 68, 90]),
(Seedance2p0BytePlusUltraMini, SevenTwentyP, [55, 110, 165, 219]),
] {
let mut builder = builder(model, resolution, None);
builder.reference_videos = Some(VideoListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_reference".to_string()),
]));
builder.total_reference_video_input_seconds = Some(10);
assert_batch_prices(builder, expected);
}
}
#[test]
fn seedance_2p5_input_duration_clamping_and_fallback_scale_with_batch() {
for (model, resolution, minimum, maximum) in [
(Seedance2p5, FourEightyP, [66, 131, 196, 261], [254, 507, 761, 1014]),
(Seedance2p5, SevenTwentyP, [143, 286, 428, 571], [555, 1110, 1664, 2219]),
(Seedance2p5, TenEightyP, [347, 693, 1040, 1386], [1348, 2695, 4043, 5390]),
(Seedance2p5Ultra, FourEightyP, [78, 155, 232, 309], [300, 600, 899, 1199]),
(Seedance2p5Ultra, SevenTwentyP, [169, 338, 506, 675], [656, 1311, 1967, 2622]),
(Seedance2p5Ultra, TenEightyP, [377, 754, 1130, 1507], [1465, 2930, 4394, 5859]),
] {
for (input_seconds, expected) in [
(Some(1), minimum), (Some(4), minimum),
(None, maximum), (Some(0), maximum), (Some(30), maximum), (Some(99), maximum),
] {
let mut builder = builder(model, resolution, None);
builder.reference_videos = Some(VideoListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_reference".to_string()),
]));
builder.total_reference_video_input_seconds = input_seconds;
assert_batch_prices(builder, expected);
}
}
}
}
fn builder(model: RouterVideoModel, resolution: RouterResolution, batch: Option<u16>) -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
provider: RouterProvider::Artcraft,
model,
resolution: Some(resolution),
duration_seconds: Some(5),
video_batch_count: batch,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..Default::default()
}
}
fn assert_batch_prices(builder: GenerateVideoRequestBuilder, expected: [u64; 4]) {
for (index, expected_price) in expected.into_iter().enumerate() {
let batch = index as u16 + 1;
let mut builder = builder.clone();
builder.video_batch_count = Some(batch);
let context = format!("{:?}, {:?}, batch {batch}", builder.model, builder.resolution);
let request = builder.build2().unwrap();
let cost = request.estimate_cost().unwrap();
assert_eq!(cost.cost_in_credits, Some(expected_price), "{context}");
assert_eq!(cost.cost_in_usd_cents, Some(expected_price), "{context}");
assert_eq!(http_request(request).video_batch_count, Some(batch), "{context}");
}
}
fn http_request(request: VideoGenerationDraftOrRequest) -> OmniGenVideoCostAndGenerateRequest {
match request {
VideoGenerationDraftOrRequest::Request(request) => match request {
VideoGenerationRequest::ArtcraftSeedance2p5(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0Fast(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0Mini(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p5Ultra(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0BytePlus(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusFast(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusMini(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltra(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltraFast(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltraMini(state) => state.request,
VideoGenerationRequest::ArtcraftHappyHorse1p0(state) => state.request,
other => panic!("Unexpected request: {other:?}"),
},
other => panic!("Expected ArtCraft request: {other:?}"),
}
}
@@ -2,6 +2,8 @@ pub(crate) mod build_common;
pub(crate) mod seedance_common;
#[cfg(test)]
mod parity_tests;
#[cfg(test)]
mod kinovi_batch_tests;
pub mod grok_imagine_video;
pub mod grok_imagine_video_1p5;
pub mod flux_3;
@@ -43,4 +45,3 @@ pub mod veo_3p1_fast;
pub mod veo_3p1_lite;
pub mod vidu_q3;
pub mod vidu_q3_turbo;
@@ -3,27 +3,21 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::providers::artcraft::build_common::{
build_artcraft_omni_video_request, plan_mini_batch_count, SupportedResolutions, UltraWideSupport,
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
};
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_mini::request::ArtcraftSeedance2p0BytePlusMiniRequestState;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
pub fn build_artcraft_seedance_2p0_bp_mini(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
pub fn build_artcraft_seedance_2p0_bp_mini(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
// Seedance 2.0 Mini supports 480p and 720p only (higher resolutions downgrade
// to 720p), and all six aspect ratios including 21:9.
// Mini supports batches of 1-8, wider than the shared 1/2/4 planning.
// Take the batch out before the shared build and set it after.
let strategy = builder.request_mismatch_mitigation_strategy;
let batch_count = plan_mini_batch_count(builder.video_batch_count.take(), strategy)?;
let mut request = build_artcraft_omni_video_request(
let request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Seedance2p0BytePlusMini,
SupportedResolutions::Fast,
UltraWideSupport::Supported,
)?;
request.video_batch_count = Some(batch_count);
let state = ArtcraftSeedance2p0BytePlusMiniRequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0BytePlusMini(state)))
@@ -87,18 +81,17 @@ mod tests {
}
#[test]
fn batch_count_up_to_eight_passed_through() {
// Mini supports batches of 1-8; execution and billing must agree.
for batch in 1..=8u16 {
fn batch_count_up_to_four_passed_through() {
for batch in 1..=4u16 {
let req = unwrap_request(builder_with(move |b| { b.video_batch_count = Some(batch); }));
assert_eq!(req.request.video_batch_count, Some(batch));
}
}
#[test]
fn batch_count_above_eight_clamps_to_eight() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(9); }));
assert_eq!(req.request.video_batch_count, Some(8));
fn batch_count_above_four_clamps_to_four() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(8); }));
assert_eq!(req.request.video_batch_count, Some(4));
}
#[test]
@@ -108,18 +108,13 @@ mod tests {
mod batch_pricing {
use super::*;
/// Mini bills batches of 1-8 in full (the platform's 1/2/4 cap does not
/// apply). Once, batch 8 executed in full but billed as batch 4.
/// ArtCraft exposes batches of 1-4 and bills the entire planned batch.
#[test]
fn every_batch_size_720p_5s() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 46);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 2, false), 91);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 3, false), 137);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, false), 182);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 5, false), 228);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 6, false), 273);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 7, false), 319);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 364);
}
#[test]
@@ -128,23 +123,16 @@ mod tests {
assert_eq!(cents(RouterResolution::FourEightyP, 5, 2, false), 36);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 3, false), 54);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 4, false), 71);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 5, false), 89);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 6, false), 107);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 7, false), 125);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 8, false), 142);
}
#[test]
fn batch_eight_with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, true), 438);
fn batch_four_with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, true), 219);
}
#[test]
fn batch_above_eight_clamps_to_eight() {
assert_eq!(
cents(RouterResolution::SevenTwentyP, 5, 9, false),
cents(RouterResolution::SevenTwentyP, 5, 8, false),
);
fn batch_above_four_clamps_to_four() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 182);
}
}
@@ -160,7 +148,7 @@ mod tests {
];
for res in resolutions {
for dur in [4u16, 5, 10, 15] {
for batch in [1u16, 2, 4] {
for batch in 1..=4 {
for has_ref in [false, true] {
let cost = build_cost(res, dur, batch, has_ref);
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
@@ -3,27 +3,21 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::providers::artcraft::build_common::{
build_artcraft_omni_video_request, plan_mini_batch_count, SupportedResolutions, UltraWideSupport,
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
};
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu_mini::request::ArtcraftSeedance2p0BytePlusUltraMiniRequestState;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
pub fn build_artcraft_seedance_2p0_bpu_mini(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
pub fn build_artcraft_seedance_2p0_bpu_mini(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
// Seedance 2.0 Mini supports 480p and 720p only (higher resolutions downgrade
// to 720p), and all six aspect ratios including 21:9.
// Mini supports batches of 1-8, wider than the shared 1/2/4 planning.
// Take the batch out before the shared build and set it after.
let strategy = builder.request_mismatch_mitigation_strategy;
let batch_count = plan_mini_batch_count(builder.video_batch_count.take(), strategy)?;
let mut request = build_artcraft_omni_video_request(
let request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Seedance2p0BytePlusUltraMini,
SupportedResolutions::Fast,
UltraWideSupport::Supported,
)?;
request.video_batch_count = Some(batch_count);
let state = ArtcraftSeedance2p0BytePlusUltraMiniRequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltraMini(state)))
@@ -87,18 +81,17 @@ mod tests {
}
#[test]
fn batch_count_up_to_eight_passed_through() {
// Mini supports batches of 1-8; execution and billing must agree.
for batch in 1..=8u16 {
fn batch_count_up_to_four_passed_through() {
for batch in 1..=4u16 {
let req = unwrap_request(builder_with(move |b| { b.video_batch_count = Some(batch); }));
assert_eq!(req.request.video_batch_count, Some(batch));
}
}
#[test]
fn batch_count_above_eight_clamps_to_eight() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(9); }));
assert_eq!(req.request.video_batch_count, Some(8));
fn batch_count_above_four_clamps_to_four() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(8); }));
assert_eq!(req.request.video_batch_count, Some(4));
}
#[test]
@@ -108,18 +108,13 @@ mod tests {
mod batch_pricing {
use super::*;
/// Mini bills batches of 1-8 in full (the platform's 1/2/4 cap does not
/// apply). Once, batch 8 executed in full but billed as batch 4.
/// ArtCraft exposes batches of 1-4 and bills the entire planned batch.
#[test]
fn every_batch_size_720p_5s() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 46);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 2, false), 91);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 3, false), 137);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, false), 182);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 5, false), 228);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 6, false), 273);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 7, false), 319);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 364);
}
#[test]
@@ -128,23 +123,16 @@ mod tests {
assert_eq!(cents(RouterResolution::FourEightyP, 5, 2, false), 36);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 3, false), 54);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 4, false), 71);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 5, false), 89);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 6, false), 107);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 7, false), 125);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 8, false), 142);
}
#[test]
fn batch_eight_with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, true), 438);
fn batch_four_with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, true), 219);
}
#[test]
fn batch_above_eight_clamps_to_eight() {
assert_eq!(
cents(RouterResolution::SevenTwentyP, 5, 9, false),
cents(RouterResolution::SevenTwentyP, 5, 8, false),
);
fn batch_above_four_clamps_to_four() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 182);
}
}
@@ -160,7 +148,7 @@ mod tests {
];
for res in resolutions {
for dur in [4u16, 5, 10, 15] {
for batch in [1u16, 2, 4] {
for batch in 1..=4 {
for has_ref in [false, true] {
let cost = build_cost(res, dur, batch, has_ref);
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
@@ -3,27 +3,21 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::providers::artcraft::build_common::{
build_artcraft_omni_video_request, plan_mini_batch_count, SupportedResolutions, UltraWideSupport,
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
};
use crate::generate::generate_video::providers::artcraft::seedance_2p0_mini::request::ArtcraftSeedance2p0MiniRequestState;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
pub fn build_artcraft_seedance_2p0_mini(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
pub fn build_artcraft_seedance_2p0_mini(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
// Seedance 2.0 Mini supports 480p and 720p only (higher resolutions downgrade
// to 720p), and all six aspect ratios including 21:9.
// Mini supports batches of 1-8, wider than the shared 1/2/4 planning.
// Take the batch out before the shared build and set it after.
let strategy = builder.request_mismatch_mitigation_strategy;
let batch_count = plan_mini_batch_count(builder.video_batch_count.take(), strategy)?;
let mut request = build_artcraft_omni_video_request(
let request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Seedance2p0Mini,
SupportedResolutions::Fast,
UltraWideSupport::Supported,
)?;
request.video_batch_count = Some(batch_count);
let state = ArtcraftSeedance2p0MiniRequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0Mini(state)))
@@ -87,18 +81,17 @@ mod tests {
}
#[test]
fn batch_count_up_to_eight_passed_through() {
// Mini supports batches of 1-8; execution and billing must agree.
for batch in 1..=8u16 {
fn batch_count_up_to_four_passed_through() {
for batch in 1..=4u16 {
let req = unwrap_request(builder_with(move |b| { b.video_batch_count = Some(batch); }));
assert_eq!(req.request.video_batch_count, Some(batch));
}
}
#[test]
fn batch_count_above_eight_clamps_to_eight() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(9); }));
assert_eq!(req.request.video_batch_count, Some(8));
fn batch_count_above_four_clamps_to_four() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(8); }));
assert_eq!(req.request.video_batch_count, Some(4));
}
#[test]
@@ -108,18 +108,13 @@ mod tests {
mod batch_pricing {
use super::*;
/// Mini bills batches of 1-8 in full (the platform's 1/2/4 cap does not
/// apply). Once, batch 8 executed in full but billed as batch 4.
/// ArtCraft exposes batches of 1-4 and bills the entire planned batch.
#[test]
fn every_batch_size_720p_5s() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 45);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 2, false), 89);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 3, false), 134);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, false), 178);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 5, false), 223);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 6, false), 267);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 7, false), 312);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 356);
}
#[test]
@@ -128,23 +123,16 @@ mod tests {
assert_eq!(cents(RouterResolution::FourEightyP, 5, 2, false), 35);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 3, false), 52);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 4, false), 69);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 5, false), 87);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 6, false), 104);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 7, false), 121);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 8, false), 138);
}
#[test]
fn batch_eight_with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, true), 429);
fn batch_four_with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, true), 215);
}
#[test]
fn batch_above_eight_clamps_to_eight() {
assert_eq!(
cents(RouterResolution::SevenTwentyP, 5, 9, false),
cents(RouterResolution::SevenTwentyP, 5, 8, false),
);
fn batch_above_four_clamps_to_four() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 178);
}
}
@@ -160,7 +148,7 @@ mod tests {
];
for res in resolutions {
for dur in [4u16, 5, 10, 15] {
for batch in [1u16, 2, 4] {
for batch in 1..=4 {
for has_ref in [false, true] {
let cost = build_cost(res, dur, batch, has_ref);
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
@@ -43,10 +43,8 @@ pub fn build_artcraft_seedance_2p5(mut builder: GenerateVideoRequestBuilder) ->
}
// 2.5 allows 4-30 second durations (longer than the shared 4-15 range in
// build_common) and generates a single video per request. Plan both here,
// leaving None for the shared builder, then set them after.
// build_common). Plan duration here and use the shared 1-4 batch planner.
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
plan_batch_count(builder.video_batch_count.take(), strategy)?;
let total_input_seconds = builder.total_reference_video_input_seconds.take();
let generate_audio = builder.generate_audio.take();
@@ -58,7 +56,6 @@ pub fn build_artcraft_seedance_2p5(mut builder: GenerateVideoRequestBuilder) ->
UltraWideSupport::Supported,
)?;
request.duration_seconds = duration_seconds;
request.video_batch_count = Some(1);
request.generate_audio = generate_audio;
let state = ArtcraftSeedance2p5RequestState { request, total_input_seconds };
@@ -87,28 +84,6 @@ fn plan_duration(
}
}
// Seedance 2.5 generates a single video per request (no batching).
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<(), ArtcraftRouterError> {
let count = video_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(()),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "video_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(()),
},
}
}
#[cfg(test)]
mod tests {
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
@@ -153,9 +128,11 @@ mod tests {
}
#[test]
fn batch_count_is_always_one() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(4); }));
assert_eq!(req.request.video_batch_count, Some(1));
fn batch_counts_one_through_four_are_preserved() {
for batch_count in 1..=4 {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(batch_count); }));
assert_eq!(req.request.video_batch_count, Some(batch_count));
}
}
#[test]
@@ -8,10 +8,11 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p5::request:
/// — when reference videos are attached — the total seconds of reference
/// video input, which are billed on top of the output duration (at a lower
/// per-second rate). The TOTAL input duration clamps to the 4..=30 second
/// billing range. No batching.
/// billing range. Batch count multiplies the total before rounding.
pub struct ArtcraftSeedance2p5CostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
pub batch_count: u16,
pub has_video_references: bool,
pub maybe_total_input_seconds: Option<u16>,
}
@@ -29,6 +30,7 @@ impl ArtcraftSeedance2p5CostState {
Self {
resolution,
duration_seconds,
batch_count: request.request.video_batch_count.unwrap_or(1),
has_video_references,
maybe_total_input_seconds: request.total_input_seconds,
}
@@ -43,6 +45,7 @@ impl ArtcraftSeedance2p5CostState {
let usd_cents = seedance_2p5_usd_cents(
self.resolution,
self.duration_seconds,
self.batch_count,
self.has_video_references,
maybe_total_input_seconds,
);
@@ -43,10 +43,8 @@ pub fn build_artcraft_seedance_2p5_u(mut builder: GenerateVideoRequestBuilder) -
}
// 2.5 allows 4-30 second durations (longer than the shared 4-15 range in
// build_common) and generates a single video per request. Plan both here,
// leaving None for the shared builder, then set them after.
// build_common). Plan duration here and use the shared 1-4 batch planner.
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
plan_batch_count(builder.video_batch_count.take(), strategy)?;
let total_input_seconds = builder.total_reference_video_input_seconds.take();
let generate_audio = builder.generate_audio.take();
@@ -58,7 +56,6 @@ pub fn build_artcraft_seedance_2p5_u(mut builder: GenerateVideoRequestBuilder) -
UltraWideSupport::Supported,
)?;
request.duration_seconds = duration_seconds;
request.video_batch_count = Some(1);
request.generate_audio = generate_audio;
let state = ArtcraftSeedance2p5UltraRequestState { request, total_input_seconds };
@@ -87,28 +84,6 @@ fn plan_duration(
}
}
// Seedance 2.5 generates a single video per request (no batching).
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<(), ArtcraftRouterError> {
let count = video_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(()),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "video_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(()),
},
}
}
#[cfg(test)]
mod tests {
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
@@ -153,9 +128,11 @@ mod tests {
}
#[test]
fn batch_count_is_always_one() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(4); }));
assert_eq!(req.request.video_batch_count, Some(1));
fn batch_counts_one_through_four_are_preserved() {
for batch_count in 1..=4 {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(batch_count); }));
assert_eq!(req.request.video_batch_count, Some(batch_count));
}
}
#[test]
@@ -9,10 +9,11 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p5_u::reques
/// reference video input, which are billed on top of the output duration (at
/// a lower per-second rate). Input seconds are clamped to
/// [`MAX_BILLED_INPUT_SECONDS`] — the model accepts at most 30 seconds of
/// video. No batching.
/// video. Batch count multiplies the total before rounding.
pub struct ArtcraftSeedance2p5UltraCostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
pub batch_count: u16,
pub has_video_references: bool,
pub maybe_total_input_seconds: Option<u16>,
}
@@ -30,6 +31,7 @@ impl ArtcraftSeedance2p5UltraCostState {
Self {
resolution,
duration_seconds,
batch_count: request.request.video_batch_count.unwrap_or(1),
has_video_references,
maybe_total_input_seconds: request.total_input_seconds,
}
@@ -44,6 +46,7 @@ impl ArtcraftSeedance2p5UltraCostState {
let usd_cents = seedance_2p5_ultra_usd_cents(
self.resolution,
self.duration_seconds,
self.batch_count,
self.has_video_references,
maybe_total_input_seconds,
);
@@ -155,10 +155,11 @@ const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 38.49728574;
/// seconds = output duration. With video references, the per-second rate
/// drops but the billed seconds are the output duration PLUS the total
/// seconds of reference video input (clamped to the 4..=30 second billing
/// range). The fractional total is rounded UP to a whole cent. No batching.
/// range). Multiply by batch count before rounding UP to a whole cent.
pub fn seedance_2p5_usd_cents(
resolution: CommonResolution,
duration_seconds: u16,
batch_count: u16,
has_video_references: bool,
maybe_total_input_seconds: Option<u16>,
) -> u64 {
@@ -193,7 +194,7 @@ pub fn seedance_2p5_usd_cents(
(rate, u64::from(duration_seconds))
};
(cents_per_second * billed_seconds as f64).ceil() as u64
(cents_per_second * billed_seconds as f64 * batch_count as f64).ceil() as u64
}
/// Seedance 2.5 Ultra — 480p price, USD cents per second.
@@ -217,11 +218,12 @@ const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 41.844875
/// this fallback is only a failsafe). Without video references, billed
/// seconds = output duration. With video references, the per-second rate
/// drops but the billed seconds are the output duration PLUS the total
/// seconds of reference video input. The fractional total is rounded UP to
/// a whole cent. No batching.
/// seconds of reference video input. Multiply by batch count before rounding
/// UP to a whole cent.
pub fn seedance_2p5_ultra_usd_cents(
resolution: CommonResolution,
duration_seconds: u16,
batch_count: u16,
has_video_references: bool,
maybe_total_input_seconds: Option<u16>,
) -> u64 {
@@ -256,7 +258,7 @@ pub fn seedance_2p5_ultra_usd_cents(
(rate, u64::from(duration_seconds))
};
(cents_per_second * billed_seconds as f64).ceil() as u64
(cents_per_second * billed_seconds as f64 * batch_count as f64).ceil() as u64
}
#[allow(clippy::too_many_arguments)]
@@ -134,6 +134,7 @@ fn plan_batch_count(
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(None),
2 => Ok(Some(KinoviHappyHorse1p0BatchCount::Two)),
3 => Ok(Some(KinoviHappyHorse1p0BatchCount::Three)),
4 => Ok(Some(KinoviHappyHorse1p0BatchCount::Four)),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
@@ -143,10 +144,10 @@ fn plan_batch_count(
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
Ok(Some(if count >= 4 { KinoviHappyHorse1p0BatchCount::Four } else { KinoviHappyHorse1p0BatchCount::Two }))
Ok(Some(KinoviHappyHorse1p0BatchCount::Four))
}
RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(if count <= 2 { KinoviHappyHorse1p0BatchCount::Two } else { KinoviHappyHorse1p0BatchCount::Four }))
Ok(Some(KinoviHappyHorse1p0BatchCount::Four))
}
},
}
@@ -177,6 +178,24 @@ mod tests {
// ── Materialized field conversions ──
#[test]
fn supported_batches_reach_cost_estimation() {
use crate::generate::generate_video::providers::kinovi::happy_horse_1p0::cost::KinoviHappyHorse1p0CostState;
for count in 1..=4 {
let builder = GenerateVideoRequestBuilder {
video_batch_count: Some(count),
duration_seconds: Some(4),
resolution: Some(RouterResolution::SevenTwentyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..happy_horse_builder()
};
let draft = unwrap_draft(build_kinovi_happy_horse_1p0(builder));
let cost = KinoviHappyHorse1p0CostState::from_draft(&draft).estimate_cost();
assert_eq!(cost.cost_in_credits, Some((132.0 * f64::from(count)).round() as u64), "batch {count}");
}
}
mod materialized_field_conversions {
use super::*;
@@ -153,7 +153,7 @@ fn plan_output_resolution(
}
}
// Seedance2p0 supports batch counts of 1 through 4.
// Seedance2p0 supports batch counts of 1 through 8.
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
@@ -165,7 +165,11 @@ fn plan_batch_count(
2 => Ok(KinoviBatchCount::Two),
3 => Ok(KinoviBatchCount::Three),
4 => Ok(KinoviBatchCount::Four),
// 5 and above:
5 => Ok(KinoviBatchCount::Five),
6 => Ok(KinoviBatchCount::Six),
7 => Ok(KinoviBatchCount::Seven),
8 => Ok(KinoviBatchCount::Eight),
// 9 and above:
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
@@ -173,8 +177,8 @@ fn plan_batch_count(
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(KinoviBatchCount::Four),
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(KinoviBatchCount::Four),
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(KinoviBatchCount::Eight),
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(KinoviBatchCount::Eight),
},
}
}
@@ -239,6 +243,24 @@ mod tests {
// ── Materialized field conversions ──
#[test]
fn supported_batches_reach_cost_estimation() {
use crate::generate::generate_video::providers::kinovi::seedance_2p0::cost::KinoviSeedance2p0CostState;
for count in 1..=8 {
let builder = GenerateVideoRequestBuilder {
video_batch_count: Some(count),
duration_seconds: Some(4),
resolution: Some(RouterResolution::FourEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let cost = KinoviSeedance2p0CostState::from_draft(&draft).estimate_cost();
assert_eq!(cost.cost_in_credits, Some((60.0 * f64::from(count)).round() as u64), "batch {count}");
}
}
mod materialized_field_conversions {
use super::*;
@@ -167,7 +167,7 @@ fn plan_output_resolution(
}
}
// Seedance2p0Fast supports batch counts of 1 through 4.
// Seedance2p0Fast supports batch counts of 1 through 8.
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
@@ -179,7 +179,11 @@ fn plan_batch_count(
2 => Ok(KinoviBatchCount::Two),
3 => Ok(KinoviBatchCount::Three),
4 => Ok(KinoviBatchCount::Four),
// 5 and above:
5 => Ok(KinoviBatchCount::Five),
6 => Ok(KinoviBatchCount::Six),
7 => Ok(KinoviBatchCount::Seven),
8 => Ok(KinoviBatchCount::Eight),
// 9 and above:
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
@@ -187,8 +191,8 @@ fn plan_batch_count(
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(KinoviBatchCount::Four),
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(KinoviBatchCount::Four),
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(KinoviBatchCount::Eight),
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(KinoviBatchCount::Eight),
},
}
}
@@ -255,6 +259,24 @@ mod tests {
// ── Materialized field conversions ──
#[test]
fn supported_batches_reach_cost_estimation() {
use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::cost::KinoviSeedance2p0FastCostState;
for count in 1..=8 {
let builder = GenerateVideoRequestBuilder {
video_batch_count: Some(count),
duration_seconds: Some(4),
resolution: Some(RouterResolution::FourEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft).estimate_cost();
assert_eq!(cost.cost_in_credits, Some((42.0 * f64::from(count)).round() as u64), "batch {count}");
}
}
mod materialized_field_conversions {
use super::*;
@@ -1,5 +1,6 @@
use kinovi_web_client::generate::video::generate_seedance_2p5::{
KinoviSeedance2p5AspectRatio as KinoviAspectRatio,
KinoviSeedance2p5BatchCount as KinoviBatchCount,
KinoviSeedance2p5Bitrate,
KinoviSeedance2p5OutputFormat,
KinoviSeedance2p5OutputResolution as KinoviOutputResolution,
@@ -61,7 +62,7 @@ fn do_build_kinovi_seedance_2p5(mut builder: GenerateVideoRequestBuilder) -> Res
// The planned value below only applies to reference mode.
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
let resolution = plan_output_resolution(builder.resolution.take(), strategy)?;
plan_batch_count(builder.video_batch_count.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 prompt = builder.prompt.take().unwrap_or_default();
@@ -82,6 +83,7 @@ fn do_build_kinovi_seedance_2p5(mut builder: GenerateVideoRequestBuilder) -> Res
};
Ok(KinoviSeedance2p5DraftState {
batch_count,
aspect_ratio,
resolution,
duration_seconds,
@@ -210,16 +212,23 @@ fn plan_output_resolution(
}
}
// Seedance 2.5 generates a single video per request (no batching).
// Seedance 2.5 supports batches of 1–8 videos.
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<(), ArtcraftRouterError> {
) -> Result<KinoviBatchCount, ArtcraftRouterError> {
let count = video_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(()),
// Over the maximum of 1.
1 => Ok(KinoviBatchCount::One),
2 => Ok(KinoviBatchCount::Two),
3 => Ok(KinoviBatchCount::Three),
4 => Ok(KinoviBatchCount::Four),
5 => Ok(KinoviBatchCount::Five),
6 => Ok(KinoviBatchCount::Six),
7 => Ok(KinoviBatchCount::Seven),
8 => Ok(KinoviBatchCount::Eight),
// Over the maximum of 8.
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
@@ -228,7 +237,7 @@ fn plan_batch_count(
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(()),
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(KinoviBatchCount::Eight),
},
}
}
@@ -355,6 +364,24 @@ mod tests {
}
}
#[test]
fn supported_batches_reach_cost_estimation() {
use crate::generate::generate_video::providers::kinovi::seedance_2p5::cost::KinoviSeedance2p5CostState;
for count in 1..=8 {
let builder = GenerateVideoRequestBuilder {
video_batch_count: Some(count),
duration_seconds: Some(4),
resolution: Some(RouterResolution::FourEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p5(builder));
let cost = KinoviSeedance2p5CostState::from_draft(&draft).estimate_cost();
assert_eq!(cost.cost_in_credits, Some((104.0 * f64::from(count)).round() as u64), "batch {count}");
}
}
mod materialized_field_conversions {
use super::*;
@@ -1,5 +1,5 @@
use kinovi_web_client::generate::video::generate_seedance_2p5::{
GenerateSeedance2p5Request, KinoviSeedance2p5Modality, KinoviSeedance2p5OutputResolution,
GenerateSeedance2p5Request, KinoviSeedance2p5BatchCount, KinoviSeedance2p5Modality, KinoviSeedance2p5OutputResolution,
MAX_BILLED_INPUT_SECONDS, MIN_BILLED_INPUT_SECONDS,
};
use kinovi_web_client::pricing::kinovi_cost_calculator_trait::KinoviCostCalculatorTrait;
@@ -18,6 +18,7 @@ use crate::generate::generate_video::providers::kinovi::seedance_2p5::request::K
pub struct KinoviSeedance2p5CostState {
pub resolution: Option<KinoviSeedance2p5OutputResolution>,
pub duration_seconds: u8,
pub batch_count: Option<KinoviSeedance2p5BatchCount>,
pub has_video_references: bool,
pub total_input_seconds: Option<u8>,
}
@@ -32,6 +33,7 @@ impl KinoviSeedance2p5CostState {
Self {
resolution: request.request.output_resolution,
duration_seconds: request.request.duration_seconds,
batch_count: request.request.batch_count,
has_video_references,
total_input_seconds: request.request.total_input_seconds,
}
@@ -49,6 +51,7 @@ impl KinoviSeedance2p5CostState {
Self {
resolution: draft.resolution,
duration_seconds: draft.duration_seconds,
batch_count: Some(draft.batch_count),
has_video_references,
total_input_seconds: draft.total_input_seconds,
}
@@ -57,7 +60,7 @@ impl KinoviSeedance2p5CostState {
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Delegate to the kinovi_web_client calculator via a pricing-only
// request. Only the modality's video references, the resolution, the
// duration, and the input seconds affect the price.
// duration, batch count, and the input seconds affect the price.
let modality = if self.has_video_references {
KinoviSeedance2p5Modality::Reference {
aspect_ratio: None,
@@ -89,6 +92,7 @@ impl KinoviSeedance2p5CostState {
modality,
output_resolution: self.resolution,
duration_seconds: self.duration_seconds,
batch_count: self.batch_count,
total_input_seconds,
use_face_blur_hack: None,
maybe_bitrate: None,
@@ -230,7 +234,7 @@ mod tests {
has_video_references: bool,
total_input_seconds: Option<u8>,
) -> KinoviSeedance2p5CostState {
KinoviSeedance2p5CostState { resolution, duration_seconds, has_video_references, total_input_seconds }
KinoviSeedance2p5CostState { batch_count: None, resolution, duration_seconds, has_video_references, total_input_seconds }
}
fn credits(
@@ -1,5 +1,5 @@
use kinovi_web_client::generate::video::generate_seedance_2p5::{
GenerateSeedance2p5Request, KinoviSeedance2p5AspectRatio, KinoviSeedance2p5Bitrate, KinoviSeedance2p5Modality,
GenerateSeedance2p5Request, KinoviSeedance2p5BatchCount, KinoviSeedance2p5AspectRatio, KinoviSeedance2p5Bitrate, KinoviSeedance2p5Modality,
KinoviSeedance2p5OutputFormat, KinoviSeedance2p5OutputResolution,
};
@@ -27,6 +27,7 @@ pub struct KinoviSeedance2p5DraftState {
pub resolution: Option<KinoviSeedance2p5OutputResolution>,
pub duration_seconds: u8,
pub batch_count: KinoviSeedance2p5BatchCount,
/// Calculation-only: total seconds of reference video input, summed across
/// all reference videos (each rounded up to a whole second).
@@ -113,6 +114,7 @@ impl KinoviSeedance2p5DraftState {
modality,
output_resolution: self.resolution,
duration_seconds: self.duration_seconds,
batch_count: Some(self.batch_count),
total_input_seconds: self.total_input_seconds,
use_face_blur_hack: None,
maybe_bitrate: self.maybe_bitrate,
@@ -1,5 +1,6 @@
use kinovi_web_client::generate::video::generate_seedance_2p5_preview::{
KinoviSeedance2p5PreviewAspectRatio as KinoviAspectRatio,
KinoviSeedance2p5PreviewBatchCount as KinoviBatchCount,
KinoviSeedance2p5PreviewOutputResolution as KinoviOutputResolution,
};
@@ -47,7 +48,7 @@ fn do_build_kinovi_seedance_2p5_preview(mut builder: GenerateVideoRequestBuilder
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
let resolution = plan_output_resolution(builder.resolution.take(), strategy)?;
plan_batch_count(builder.video_batch_count.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 prompt = builder.prompt.take().unwrap_or_default();
@@ -61,6 +62,7 @@ fn do_build_kinovi_seedance_2p5_preview(mut builder: GenerateVideoRequestBuilder
};
Ok(KinoviSeedance2p5PreviewDraftState {
batch_count,
aspect_ratio,
resolution,
duration_seconds,
@@ -177,16 +179,23 @@ fn plan_output_resolution(
}
}
// Seedance 2.5 Preview generates a single video per request (no batching).
// Seedance 2.5 Preview supports batches of 1–8 videos.
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<(), ArtcraftRouterError> {
) -> Result<KinoviBatchCount, ArtcraftRouterError> {
let count = video_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(()),
// Over the maximum of 1.
1 => Ok(KinoviBatchCount::One),
2 => Ok(KinoviBatchCount::Two),
3 => Ok(KinoviBatchCount::Three),
4 => Ok(KinoviBatchCount::Four),
5 => Ok(KinoviBatchCount::Five),
6 => Ok(KinoviBatchCount::Six),
7 => Ok(KinoviBatchCount::Seven),
8 => Ok(KinoviBatchCount::Eight),
// Over the maximum of 8.
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
@@ -195,7 +204,7 @@ fn plan_batch_count(
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(()),
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(KinoviBatchCount::Eight),
},
}
}
@@ -248,6 +257,24 @@ mod tests {
use super::*;
#[test]
fn supported_batches_reach_cost_estimation() {
use crate::generate::generate_video::providers::kinovi::seedance_2p5_preview::cost::KinoviSeedance2p5PreviewCostState;
for count in 1..=8 {
let builder = GenerateVideoRequestBuilder {
video_batch_count: Some(count),
duration_seconds: Some(4),
resolution: Some(RouterResolution::FourEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..preview_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
let cost = KinoviSeedance2p5PreviewCostState::from_draft(&draft).estimate_cost();
assert_eq!(cost.cost_in_credits, Some((168.52 * f64::from(count)).round() as u64), "batch {count}");
}
}
mod materialized_field_conversions {
use super::*;
@@ -346,19 +373,20 @@ mod tests {
}
#[test]
fn batch_over_one_downgrades_to_single() {
fn batch_over_eight_downgrades_to_eight() {
let builder = GenerateVideoRequestBuilder {
video_batch_count: Some(4),
video_batch_count: Some(9),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
..preview_builder()
};
assert!(build_kinovi_seedance_2p5_preview(builder).is_ok());
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Eight));
}
#[test]
fn batch_over_one_error_out() {
fn batch_over_eight_error_out() {
let builder = GenerateVideoRequestBuilder {
video_batch_count: Some(4),
video_batch_count: Some(9),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..preview_builder()
};
@@ -1,18 +1,18 @@
use kinovi_web_client::pricing::kinovi_cost_calculator_trait::KinoviCostCalculatorTrait;
use kinovi_web_client::generate::video::generate_seedance_2p5_preview::{
GenerateSeedance2p5PreviewRequest, KinoviSeedance2p5PreviewOutputResolution,
GenerateSeedance2p5PreviewRequest, KinoviSeedance2p5PreviewBatchCount, KinoviSeedance2p5PreviewOutputResolution,
};
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::kinovi::seedance_2p5_preview::draft::KinoviSeedance2p5PreviewDraftState;
use crate::generate::generate_video::providers::kinovi::seedance_2p5_preview::request::KinoviSeedance2p5PreviewRequestState;
/// Only the resolution and duration matter — Seedance 2.5 Preview has no
/// video-reference surcharge and no batching, so references never change the
/// price.
/// Resolution, duration, and batch count determine the price.
/// Seedance 2.5 Preview has no video-reference surcharge.
pub struct KinoviSeedance2p5PreviewCostState {
pub resolution: Option<KinoviSeedance2p5PreviewOutputResolution>,
pub duration_seconds: u8,
pub batch_count: Option<KinoviSeedance2p5PreviewBatchCount>,
}
impl KinoviSeedance2p5PreviewCostState {
@@ -20,6 +20,7 @@ impl KinoviSeedance2p5PreviewCostState {
Self {
resolution: request.request.output_resolution,
duration_seconds: request.request.duration_seconds,
batch_count: request.request.batch_count,
}
}
@@ -27,6 +28,7 @@ impl KinoviSeedance2p5PreviewCostState {
Self {
resolution: draft.resolution,
duration_seconds: draft.duration_seconds,
batch_count: Some(draft.batch_count),
}
}
@@ -34,6 +36,7 @@ impl KinoviSeedance2p5PreviewCostState {
let pricing_request = GenerateSeedance2p5PreviewRequest {
output_resolution: self.resolution,
duration_seconds: self.duration_seconds,
batch_count: self.batch_count,
// No impact on price (references never affect 2.5 Preview pricing)
prompt: String::new(),
@@ -139,7 +142,7 @@ mod tests {
// ── Helpers ──
fn cost_state(resolution: Option<KinoviOutputResolution>, duration_seconds: u8) -> KinoviSeedance2p5PreviewCostState {
KinoviSeedance2p5PreviewCostState { resolution, duration_seconds }
KinoviSeedance2p5PreviewCostState { batch_count: None, resolution, duration_seconds }
}
fn credits(resolution: Option<KinoviOutputResolution>, duration_seconds: u8) -> u64 {
@@ -1,5 +1,5 @@
use kinovi_web_client::generate::video::generate_seedance_2p5_preview::{
GenerateSeedance2p5PreviewRequest, KinoviSeedance2p5PreviewAspectRatio,
GenerateSeedance2p5PreviewRequest, KinoviSeedance2p5PreviewBatchCount, KinoviSeedance2p5PreviewAspectRatio,
KinoviSeedance2p5PreviewOutputResolution,
};
@@ -20,6 +20,7 @@ pub struct KinoviSeedance2p5PreviewDraftState {
pub aspect_ratio: KinoviSeedance2p5PreviewAspectRatio,
pub resolution: Option<KinoviSeedance2p5PreviewOutputResolution>,
pub duration_seconds: u8,
pub batch_count: KinoviSeedance2p5PreviewBatchCount,
pub unhandled_request_state: Option<KinoviSeedance2p5PreviewRemainingItems>,
}
@@ -67,6 +68,7 @@ impl KinoviSeedance2p5PreviewDraftState {
aspect_ratio: Some(self.aspect_ratio),
output_resolution: self.resolution,
duration_seconds: self.duration_seconds,
batch_count: Some(self.batch_count),
reference_image_urls,
reference_video_urls,
reference_audio_urls,
@@ -0,0 +1,96 @@
# Wan 3.0 pricing and September 23 promotion
Last verified: **2026-09-18**. Recheck after **2026-09-23** (starting September 24).
The code and tests intentionally use the promotional Wan 3.0 rates below.
Prices stay at these values until a reviewed code change; there is no automatic
date-based switch. Kinovi advertises 30% off until September 23 without specifying
the cutoff time or timezone. Confirm the live prices when updating.
[Kinovi promotion and pricing](https://kinovi.ai/models/wan3-text-to-video)
## Rates currently used
All values are **Kinovi credits per output second**. Each family's rates apply
to text-to-video, image-to-video, and reference-to-video. Reference media does
not add billed input seconds. Sources for regular Wan:
[text](https://kinovi.ai/models/wan3-text-to-video),
[image](https://kinovi.ai/models/wan3-image-to-video),
[reference](https://kinovi.ai/models/wan3-ref-to-video).
| Family | Resolution | Credits/second | Current status |
|---------------|------------|----------------|--------------------------------|
| Wan 3.0 | 480p | 9.98 | Promotional until Sep 23 |
| Wan 3.0 | 720p | 19.96 | Promotional until Sep 23 |
| Wan 3.0 | 1080p | 39.91 | Promotional until Sep 23 |
| Wan 3.0 Prime | 480p | 18.86 | No advertised Sep 23 promotion |
| Wan 3.0 Prime | 720p | 38.83 | No advertised Sep 23 promotion |
| Wan 3.0 Prime | 1080p | 77.66 | No advertised Sep 23 promotion |
Prime sources:
[text](https://kinovi.ai/models/wan3-prime-text-to-video),
[image](https://kinovi.ai/models/wan3-prime-image-to-video),
[reference](https://kinovi.ai/models/wan3-prime-ref-to-video).
Prime 480p for 30 seconds is **565.8 credits**; the original supplied reference
example's 568.8 was a typo. Recheck Prime independently; the regular Wan
promotion ending does not establish a Prime price change.
The promotional rates already include the discount. Total credits are the
selected rate multiplied by output seconds. Both pricing tiers currently use
the same credit rates; only their credit purchase rates differ. The client uses
192.98 credits/USD for Consumer and 243.16 for Enterprise, defined in
[pricing constants](src/pricing/constants.rs).
[KinoviPricingTier::cost_from_credits](src/pricing/kinovi_pricing_tier.rs) rounds
the total to hundredths of a credit and calculates fractional, floor, and ceiling
USD cents. Use those purchase rates for client costs; Kinovi's displayed USD
prices use a different conversion.
## Advertised non-promotional rates to recheck
These are the crossed-out prices shown alongside the promotion on September 18.
They are **reference values for the later update**, not active calculator rates
or a guarantee of what Kinovi will charge after the promotion.
The same values appear on the regular Wan
[text](https://kinovi.ai/models/wan3-text-to-video),
[image](https://kinovi.ai/models/wan3-image-to-video), and
[reference](https://kinovi.ai/models/wan3-ref-to-video) pricing pages.
| Resolution | Current promotional credits/second | Advertised non-promotional credits/second |
|------------|------------------------------------|-------------------------------------------|
| 480p | 9.98 | 14.2571 |
| 720p | 19.96 | 28.5143 |
| 1080p | 39.91 | 57.0143 |
## How to update after September 23
1. Reopen the six model pages above and confirm the promotion ended, the exact
credit rates, and total-credit rounding. Check consumer quotes for multiple
durations and every resolution; the web client uses the consumer workflow.
Record the verification date and evidence in this document.
2. Update `WAN_CREDIT_HUNDREDTHS_PER_SECOND` and `Wan3p0Model::calculate_costs`
in [wan_3p0.rs](src/generate/video/wan_3p0.rs). The current table is
`[998, 1996, 3991]` in 480p/720p/1080p order. **The advertised replacement
rates have four decimal places:** the existing hundredths representation
cannot preserve them. If confirmed, use a sufficient fixed-point scale
(for example 10,000), rename the constants to match their units, and adjust
the final divisor. Preserve rate precision until after multiplying by
duration; verify provider rounding before changing the shared credit
conversion. If both families change scale, rescale the Prime integers while
preserving Prime's monetary rates unless a separate price change is confirmed.
3. Update the literal expectations in
[wan_3p0_tests.rs](src/generate/video/wan_3p0_tests.rs): `WAN_CREDITS`,
`WAN_CENTS`, the `199.6` regular `reference_media_tests!` argument (720p/10s),
and the `99.8` regular image `check!` argument (720p/5s). The default-price
and seed/audio tests already read the literal tables. Keep expected prices
independent of production rate constants and assert both pricing tiers.
Update Prime expectations only if its verified monetary rates change.
4. Update this document and the dated comments beside the production constants
and test tables. Keep the promotional snapshot as historical context.
5. Run from the repository root:
```sh
cargo test --offline -p kinovi_web_client --lib wan_3p0_tests
cargo test --offline -p kinovi_web_client --lib
```
The Wan HTTP tests use a local server and need permission to bind localhost.
These commands leave the existing ignored live-generation tests ignored.
@@ -49,6 +49,7 @@ pub enum KinoviHappyHorse1p0OutputResolution {
pub enum KinoviHappyHorse1p0BatchCount {
One,
Two,
Three,
Four,
}
@@ -79,6 +80,7 @@ impl GenerateHappyHorse1p0Request {
let batch_multiplier: u64 = match self.batch_count {
None | Some(KinoviHappyHorse1p0BatchCount::One) => 1,
Some(KinoviHappyHorse1p0BatchCount::Two) => 2,
Some(KinoviHappyHorse1p0BatchCount::Three) => 3,
Some(KinoviHappyHorse1p0BatchCount::Four) => 4,
};
@@ -174,6 +176,7 @@ fn map_batch_count(bc: Option<KinoviHappyHorse1p0BatchCount>) -> KinoviBatchCoun
match bc {
Some(KinoviHappyHorse1p0BatchCount::One) | None => KinoviBatchCountRaw::One,
Some(KinoviHappyHorse1p0BatchCount::Two) => KinoviBatchCountRaw::Two,
Some(KinoviHappyHorse1p0BatchCount::Three) => KinoviBatchCountRaw::Three,
Some(KinoviHappyHorse1p0BatchCount::Four) => KinoviBatchCountRaw::Four,
}
}
@@ -275,11 +278,68 @@ mod tests {
mod batch_tests {
use super::*;
const BATCHES: [KinoviHappyHorse1p0BatchCount; 4] = [
KinoviHappyHorse1p0BatchCount::One,
KinoviHappyHorse1p0BatchCount::Two,
KinoviHappyHorse1p0BatchCount::Three,
KinoviHappyHorse1p0BatchCount::Four,
];
#[test]
fn batch_1_is_base() {
let base = r720(5).calculate_costs().kinovi_credits;
let explicit = make_request(5, None, Some(KinoviHappyHorse1p0BatchCount::One)).calculate_costs().kinovi_credits;
assert_eq!(base, explicit);
fn fixed_five_second_batch_prices() {
// Each pair is (credits, USD cents rounded up), for batches 1 through 4.
let cases = [
(KinoviHappyHorse1p0OutputResolution::SevenTwentyP, [
(165, 68), (330, 136), (495, 204), (660, 272),
]),
(KinoviHappyHorse1p0OutputResolution::TenEightyP, [
(330, 136), (660, 272), (990, 408), (1320, 544),
]),
];
for (resolution, prices) in cases {
for (batch, (credits, usd_cents)) in BATCHES.into_iter().zip(prices) {
let cost = make_request(5, Some(resolution), Some(batch)).calculate_costs();
assert_eq!(
(cost.kinovi_credits, cost.usd_cents_rounded_up),
(credits, usd_cents),
"{resolution:?} {batch:?}",
);
}
}
}
#[test]
fn all_batches_scale_every_resolution_and_round_the_total() {
let batches = [
(None, 1),
(Some(KinoviHappyHorse1p0BatchCount::One), 1),
(Some(KinoviHappyHorse1p0BatchCount::Two), 2),
(Some(KinoviHappyHorse1p0BatchCount::Three), 3),
(Some(KinoviHappyHorse1p0BatchCount::Four), 4),
];
for resolution in [
None,
Some(KinoviHappyHorse1p0OutputResolution::SevenTwentyP),
Some(KinoviHappyHorse1p0OutputResolution::TenEightyP),
] {
for duration in [3, 5, 15] {
let single = make_request(duration, resolution, None).calculate_costs();
for (batch, count) in batches {
let batched = make_request(duration, resolution, batch).calculate_costs();
let context = format!("{resolution:?} {duration}s {batch:?}");
let expected_credits = single.kinovi_credits * count;
assert_eq!(batched.kinovi_credits, expected_credits, "{context}: credits");
assert!(
(batched.usd_cents_fractional - single.usd_cents_fractional * count as f64).abs() < 1e-8,
"{context}: fractional cents",
);
// Happy Horse currently uses the legacy 243 credits/dollar conversion.
let numerator = expected_credits * 100;
assert_eq!(batched.usd_cents_rounded_up, numerator.div_ceil(243), "{context}: rounded up");
assert_eq!(batched.usd_cents_rounded_down, numerator / 243, "{context}: rounded down");
}
}
}
}
#[test]
@@ -68,6 +68,10 @@ pub enum KinoviSeedance2p0BatchCount {
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
}
#[derive(Debug, Clone, Copy)]
@@ -166,6 +170,10 @@ impl KinoviCostCalculatorTrait for GenerateSeedance2p0Request {
Some(KinoviSeedance2p0BatchCount::Two) => 2.0,
Some(KinoviSeedance2p0BatchCount::Three) => 3.0,
Some(KinoviSeedance2p0BatchCount::Four) => 4.0,
Some(KinoviSeedance2p0BatchCount::Five) => 5.0,
Some(KinoviSeedance2p0BatchCount::Six) => 6.0,
Some(KinoviSeedance2p0BatchCount::Seven) => 7.0,
Some(KinoviSeedance2p0BatchCount::Eight) => 8.0,
};
let output_seconds = f64::from(self.duration_seconds) * batch_multiplier;
@@ -274,6 +282,10 @@ fn map_batch_count(bc: Option<KinoviSeedance2p0BatchCount>) -> KinoviBatchCountR
Some(KinoviSeedance2p0BatchCount::Two) => KinoviBatchCountRaw::Two,
Some(KinoviSeedance2p0BatchCount::Three) => KinoviBatchCountRaw::Three,
Some(KinoviSeedance2p0BatchCount::Four) => KinoviBatchCountRaw::Four,
Some(KinoviSeedance2p0BatchCount::Five) => KinoviBatchCountRaw::Five,
Some(KinoviSeedance2p0BatchCount::Six) => KinoviBatchCountRaw::Six,
Some(KinoviSeedance2p0BatchCount::Seven) => KinoviBatchCountRaw::Seven,
Some(KinoviSeedance2p0BatchCount::Eight) => KinoviBatchCountRaw::Eight,
}
}
@@ -612,18 +624,165 @@ mod tests {
mod batch_tests {
use super::*;
use crate::test_utils::assert_batch_cost::assert_batch_cost;
const BATCHES: [KinoviSeedance2p0BatchCount; 8] = [
KinoviSeedance2p0BatchCount::One,
KinoviSeedance2p0BatchCount::Two,
KinoviSeedance2p0BatchCount::Three,
KinoviSeedance2p0BatchCount::Four,
KinoviSeedance2p0BatchCount::Five,
KinoviSeedance2p0BatchCount::Six,
KinoviSeedance2p0BatchCount::Seven,
KinoviSeedance2p0BatchCount::Eight,
];
// Each pair is (credits, USD cents rounded up), for batches 1 through 8.
// Pin literal totals so rate changes fail independently of scaling checks.
#[test]
fn fixed_five_second_batch_prices_without_video_reference() {
let cases = [
(KinoviSeedance2p0OutputResolution::FourEightyP, KinoviPricingTier::Consumer, [
(75.0, 39), (150.0, 78), (225.0, 117), (300.0, 156),
(375.0, 195), (450.0, 234), (525.0, 273), (600.0, 311),
]),
(KinoviSeedance2p0OutputResolution::SevenTwentyP, KinoviPricingTier::Consumer, [
(200.0, 104), (400.0, 208), (600.0, 311), (800.0, 415),
(1000.0, 519), (1200.0, 622), (1400.0, 726), (1600.0, 830),
]),
(KinoviSeedance2p0OutputResolution::TenEightyP, KinoviPricingTier::Consumer, [
(450.0, 234), (900.0, 467), (1350.0, 700), (1800.0, 933),
(2250.0, 1166), (2700.0, 1400), (3150.0, 1633), (3600.0, 1866),
]),
(KinoviSeedance2p0OutputResolution::FourK, KinoviPricingTier::Consumer, [
(1000.0, 519), (2000.0, 1037), (3000.0, 1555), (4000.0, 2073),
(5000.0, 2591), (6000.0, 3110), (7000.0, 3628), (8000.0, 4146),
]),
(KinoviSeedance2p0OutputResolution::FourEightyP, KinoviPricingTier::Enterprise, [
(75.0, 31), (150.0, 62), (225.0, 93), (300.0, 124),
(375.0, 155), (450.0, 186), (525.0, 216), (600.0, 247),
]),
(KinoviSeedance2p0OutputResolution::SevenTwentyP, KinoviPricingTier::Enterprise, [
(189.5, 78), (379.0, 156), (568.5, 234), (758.0, 312),
(947.5, 390), (1137.0, 468), (1326.5, 546), (1516.0, 624),
]),
(KinoviSeedance2p0OutputResolution::TenEightyP, KinoviPricingTier::Enterprise, [
(450.0, 186), (900.0, 371), (1350.0, 556), (1800.0, 741),
(2250.0, 926), (2700.0, 1111), (3150.0, 1296), (3600.0, 1481),
]),
(KinoviSeedance2p0OutputResolution::FourK, KinoviPricingTier::Enterprise, [
(1000.0, 412), (2000.0, 823), (3000.0, 1234), (4000.0, 1646),
(5000.0, 2057), (6000.0, 2468), (7000.0, 2879), (8000.0, 3291),
]),
];
for (resolution, tier, prices) in cases {
for (batch, (credits, usd_cents)) in BATCHES.into_iter().zip(prices) {
let request = build_request(5, Some(resolution), Some(batch));
let cost = request.calculate_costs(tier).total_cost;
assert_eq!(
(cost.kinovi_credits, cost.usd_cents_rounded_up),
(credits, usd_cents),
"{resolution:?} {tier:?} {batch:?}",
);
}
}
}
#[test]
fn batch_multiplies_at_both_tiers() {
fn fixed_five_second_batch_prices_with_video_reference() {
let cases = [
(KinoviSeedance2p0OutputResolution::FourEightyP, KinoviPricingTier::Consumer, [
(95.0, 50), (190.0, 99), (285.0, 148), (380.0, 197),
(475.0, 247), (570.0, 296), (665.0, 345), (760.0, 394),
]),
(KinoviSeedance2p0OutputResolution::SevenTwentyP, KinoviPricingTier::Consumer, [
(240.0, 125), (480.0, 249), (720.0, 374), (960.0, 498),
(1200.0, 622), (1440.0, 747), (1680.0, 871), (1920.0, 995),
]),
(KinoviSeedance2p0OutputResolution::TenEightyP, KinoviPricingTier::Consumer, [
(540.0, 280), (1080.0, 560), (1620.0, 840), (2160.0, 1120),
(2700.0, 1400), (3240.0, 1679), (3780.0, 1959), (4320.0, 2239),
]),
(KinoviSeedance2p0OutputResolution::FourK, KinoviPricingTier::Consumer, [
(1200.0, 622), (2400.0, 1244), (3600.0, 1866), (4800.0, 2488),
(6000.0, 3110), (7200.0, 3731), (8400.0, 4353), (9600.0, 4975),
]),
(KinoviSeedance2p0OutputResolution::FourEightyP, KinoviPricingTier::Enterprise, [
(95.0, 40), (190.0, 79), (285.0, 118), (380.0, 157),
(475.0, 196), (570.0, 235), (665.0, 274), (760.0, 313),
]),
(KinoviSeedance2p0OutputResolution::SevenTwentyP, KinoviPricingTier::Enterprise, [
(227.4, 94), (454.8, 188), (682.2, 281), (909.6, 375),
(1137.0, 468), (1364.4, 562), (1591.8, 655), (1819.2, 749),
]),
(KinoviSeedance2p0OutputResolution::TenEightyP, KinoviPricingTier::Enterprise, [
(540.0, 223), (1080.0, 445), (1620.0, 667), (2160.0, 889),
(2700.0, 1111), (3240.0, 1333), (3780.0, 1555), (4320.0, 1777),
]),
(KinoviSeedance2p0OutputResolution::FourK, KinoviPricingTier::Enterprise, [
(1200.0, 494), (2400.0, 988), (3600.0, 1481), (4800.0, 1975),
(6000.0, 2468), (7200.0, 2962), (8400.0, 3455), (9600.0, 3949),
]),
];
for (resolution, tier, prices) in cases {
for (batch, (credits, usd_cents)) in BATCHES.into_iter().zip(prices) {
let request = build_request(5, Some(resolution), Some(batch));
let request = with_video_ref(request);
let cost = request.calculate_costs(tier).total_cost;
assert_eq!(
(cost.kinovi_credits, cost.usd_cents_rounded_up),
(credits, usd_cents),
"{resolution:?} {tier:?} {batch:?}",
);
}
}
}
#[test]
fn all_batches_scale_every_resolution_and_surcharge_at_both_tiers() {
let batches = [
(None, 1),
(Some(KinoviSeedance2p0BatchCount::One), 1),
(Some(KinoviSeedance2p0BatchCount::Two), 2),
(Some(KinoviSeedance2p0BatchCount::Three), 3),
(Some(KinoviSeedance2p0BatchCount::Four), 4),
(Some(KinoviSeedance2p0BatchCount::Five), 5),
(Some(KinoviSeedance2p0BatchCount::Six), 6),
(Some(KinoviSeedance2p0BatchCount::Seven), 7),
(Some(KinoviSeedance2p0BatchCount::Eight), 8),
];
for tier in [KinoviPricingTier::Enterprise, KinoviPricingTier::Consumer] {
let base = r720(5).calculate_costs(tier).total_cost.kinovi_credits;
for (batch, multiplier) in [
(KinoviSeedance2p0BatchCount::Two, 2.0),
(KinoviSeedance2p0BatchCount::Three, 3.0),
(KinoviSeedance2p0BatchCount::Four, 4.0),
for resolution in [
None,
Some(KinoviSeedance2p0OutputResolution::FourEightyP),
Some(KinoviSeedance2p0OutputResolution::SevenTwentyP),
Some(KinoviSeedance2p0OutputResolution::TenEightyP),
Some(KinoviSeedance2p0OutputResolution::FourK),
] {
let batched = build_request(5, None, Some(batch)).calculate_costs(tier).total_cost.kinovi_credits;
assert_eq!(batched, base * multiplier, "{batch:?} at {tier:?}");
for duration in [4, 5, 15] {
for references in [
None,
Some(vec![]),
Some(vec!["https://example.com/ref.mp4".to_string()]),
Some(vec!["https://example.com/ref1.mp4".to_string(), "https://example.com/ref2.mp4".to_string()]),
] {
let mut request = build_request(duration, resolution, None);
request.reference_video_urls = references;
let single = request.calculate_costs(tier);
for (batch, count) in batches {
request.batch_count = batch;
let batched = request.calculate_costs(tier);
let context = format!("{tier:?} {resolution:?} {duration}s {batch:?} references={:?}", request.reference_video_urls);
assert_batch_cost(single.base_cost, batched.base_cost, count, tier, &context);
assert_batch_cost(single.total_cost, batched.total_cost, count, tier, &context);
match (single.video_reference_surcharge_cost, batched.video_reference_surcharge_cost) {
(None, None) => {},
(Some(single), Some(batched)) => assert_batch_cost(single, batched, count, tier, &context),
_ => panic!("{context}: batch count changed surcharge presence"),
}
}
}
}
}
}
}
@@ -66,6 +66,10 @@ pub enum KinoviSeedance2p0FastBatchCount {
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
}
#[derive(Debug, Clone, Copy)]
@@ -140,6 +144,10 @@ impl KinoviCostCalculatorTrait for GenerateSeedance2p0FastRequest {
Some(KinoviSeedance2p0FastBatchCount::Two) => 2.0,
Some(KinoviSeedance2p0FastBatchCount::Three) => 3.0,
Some(KinoviSeedance2p0FastBatchCount::Four) => 4.0,
Some(KinoviSeedance2p0FastBatchCount::Five) => 5.0,
Some(KinoviSeedance2p0FastBatchCount::Six) => 6.0,
Some(KinoviSeedance2p0FastBatchCount::Seven) => 7.0,
Some(KinoviSeedance2p0FastBatchCount::Eight) => 8.0,
};
let output_seconds = f64::from(self.duration_seconds) * batch_multiplier;
@@ -244,6 +252,10 @@ fn map_batch_count(bc: Option<KinoviSeedance2p0FastBatchCount>) -> KinoviBatchCo
Some(KinoviSeedance2p0FastBatchCount::Two) => KinoviBatchCountRaw::Two,
Some(KinoviSeedance2p0FastBatchCount::Three) => KinoviBatchCountRaw::Three,
Some(KinoviSeedance2p0FastBatchCount::Four) => KinoviBatchCountRaw::Four,
Some(KinoviSeedance2p0FastBatchCount::Five) => KinoviBatchCountRaw::Five,
Some(KinoviSeedance2p0FastBatchCount::Six) => KinoviBatchCountRaw::Six,
Some(KinoviSeedance2p0FastBatchCount::Seven) => KinoviBatchCountRaw::Seven,
Some(KinoviSeedance2p0FastBatchCount::Eight) => KinoviBatchCountRaw::Eight,
}
}
@@ -528,18 +540,131 @@ mod tests {
mod batch_tests {
use super::*;
use crate::test_utils::assert_batch_cost::assert_batch_cost;
const BATCHES: [KinoviSeedance2p0FastBatchCount; 8] = [
KinoviSeedance2p0FastBatchCount::One,
KinoviSeedance2p0FastBatchCount::Two,
KinoviSeedance2p0FastBatchCount::Three,
KinoviSeedance2p0FastBatchCount::Four,
KinoviSeedance2p0FastBatchCount::Five,
KinoviSeedance2p0FastBatchCount::Six,
KinoviSeedance2p0FastBatchCount::Seven,
KinoviSeedance2p0FastBatchCount::Eight,
];
// Each pair is (credits, USD cents rounded up), for batches 1 through 8.
// Pin literal totals so rate changes fail independently of scaling checks.
#[test]
fn fixed_five_second_batch_prices_without_video_reference() {
let cases = [
(KinoviSeedance2p0FastOutputResolution::FourEightyP, KinoviPricingTier::Consumer, [
(70.0, 37), (140.0, 73), (210.0, 109), (280.0, 146),
(350.0, 182), (420.0, 218), (490.0, 254), (560.0, 291),
]),
(KinoviSeedance2p0FastOutputResolution::SevenTwentyP, KinoviPricingTier::Consumer, [
(140.0, 73), (280.0, 146), (420.0, 218), (560.0, 291),
(700.0, 363), (840.0, 436), (980.0, 508), (1120.0, 581),
]),
(KinoviSeedance2p0FastOutputResolution::FourEightyP, KinoviPricingTier::Enterprise, [
(52.5, 22), (105.0, 44), (157.5, 65), (210.0, 87),
(262.5, 108), (315.0, 130), (367.5, 152), (420.0, 173),
]),
(KinoviSeedance2p0FastOutputResolution::SevenTwentyP, KinoviPricingTier::Enterprise, [
(105.0, 44), (210.0, 87), (315.0, 130), (420.0, 173),
(525.0, 216), (630.0, 260), (735.0, 303), (840.0, 346),
]),
];
for (resolution, tier, prices) in cases {
for (batch, (credits, usd_cents)) in BATCHES.into_iter().zip(prices) {
let request = build_request(5, Some(resolution), Some(batch));
let cost = request.calculate_costs(tier).total_cost;
assert_eq!(
(cost.kinovi_credits, cost.usd_cents_rounded_up),
(credits, usd_cents),
"{resolution:?} {tier:?} {batch:?}",
);
}
}
}
#[test]
fn batch_multiplies_at_both_tiers() {
fn fixed_five_second_batch_prices_with_video_reference() {
let cases = [
(KinoviSeedance2p0FastOutputResolution::FourEightyP, KinoviPricingTier::Consumer, [
(90.0, 47), (180.0, 94), (270.0, 140), (360.0, 187),
(450.0, 234), (540.0, 280), (630.0, 327), (720.0, 374),
]),
(KinoviSeedance2p0FastOutputResolution::SevenTwentyP, KinoviPricingTier::Consumer, [
(170.0, 89), (340.0, 177), (510.0, 265), (680.0, 353),
(850.0, 441), (1020.0, 529), (1190.0, 617), (1360.0, 705),
]),
(KinoviSeedance2p0FastOutputResolution::FourEightyP, KinoviPricingTier::Enterprise, [
(67.5, 28), (135.0, 56), (202.5, 84), (270.0, 112),
(337.5, 139), (405.0, 167), (472.5, 195), (540.0, 223),
]),
(KinoviSeedance2p0FastOutputResolution::SevenTwentyP, KinoviPricingTier::Enterprise, [
(127.5, 53), (255.0, 105), (382.5, 158), (510.0, 210),
(637.5, 263), (765.0, 315), (892.5, 368), (1020.0, 420),
]),
];
for (resolution, tier, prices) in cases {
for (batch, (credits, usd_cents)) in BATCHES.into_iter().zip(prices) {
let request = build_request(5, Some(resolution), Some(batch));
let request = with_video_ref(request);
let cost = request.calculate_costs(tier).total_cost;
assert_eq!(
(cost.kinovi_credits, cost.usd_cents_rounded_up),
(credits, usd_cents),
"{resolution:?} {tier:?} {batch:?}",
);
}
}
}
#[test]
fn all_batches_scale_every_resolution_and_surcharge_at_both_tiers() {
let batches = [
(None, 1),
(Some(KinoviSeedance2p0FastBatchCount::One), 1),
(Some(KinoviSeedance2p0FastBatchCount::Two), 2),
(Some(KinoviSeedance2p0FastBatchCount::Three), 3),
(Some(KinoviSeedance2p0FastBatchCount::Four), 4),
(Some(KinoviSeedance2p0FastBatchCount::Five), 5),
(Some(KinoviSeedance2p0FastBatchCount::Six), 6),
(Some(KinoviSeedance2p0FastBatchCount::Seven), 7),
(Some(KinoviSeedance2p0FastBatchCount::Eight), 8),
];
for tier in [KinoviPricingTier::Enterprise, KinoviPricingTier::Consumer] {
let base = r720(5).calculate_costs(tier).total_cost.kinovi_credits;
for (batch, multiplier) in [
(KinoviSeedance2p0FastBatchCount::Two, 2.0),
(KinoviSeedance2p0FastBatchCount::Three, 3.0),
(KinoviSeedance2p0FastBatchCount::Four, 4.0),
for resolution in [
None,
Some(KinoviSeedance2p0FastOutputResolution::FourEightyP),
Some(KinoviSeedance2p0FastOutputResolution::SevenTwentyP),
] {
let batched = build_request(5, None, Some(batch)).calculate_costs(tier).total_cost.kinovi_credits;
assert_eq!(batched, base * multiplier, "{batch:?} at {tier:?}");
for duration in [4, 5, 15] {
for references in [
None,
Some(vec![]),
Some(vec!["https://example.com/ref.mp4".to_string()]),
Some(vec!["https://example.com/ref1.mp4".to_string(), "https://example.com/ref2.mp4".to_string()]),
] {
let mut request = build_request(duration, resolution, None);
request.reference_video_urls = references;
let single = request.calculate_costs(tier);
for (batch, count) in batches {
request.batch_count = batch;
let batched = request.calculate_costs(tier);
let context = format!("{tier:?} {resolution:?} {duration}s {batch:?} references={:?}", request.reference_video_urls);
assert_batch_cost(single.base_cost, batched.base_cost, count, tier, &context);
assert_batch_cost(single.total_cost, batched.total_cost, count, tier, &context);
match (single.video_reference_surcharge_cost, batched.video_reference_surcharge_cost) {
(None, None) => {},
(Some(single), Some(batched)) => assert_batch_cost(single, batched, count, tier, &context),
_ => panic!("{context}: batch count changed surcharge presence"),
}
}
}
}
}
}
}
@@ -514,24 +514,131 @@ mod tests {
mod batch_tests {
use super::*;
use crate::test_utils::assert_batch_cost::assert_batch_cost;
const BATCHES: [KinoviSeedance2p0MiniBatchCount; 8] = [
KinoviSeedance2p0MiniBatchCount::One,
KinoviSeedance2p0MiniBatchCount::Two,
KinoviSeedance2p0MiniBatchCount::Three,
KinoviSeedance2p0MiniBatchCount::Four,
KinoviSeedance2p0MiniBatchCount::Five,
KinoviSeedance2p0MiniBatchCount::Six,
KinoviSeedance2p0MiniBatchCount::Seven,
KinoviSeedance2p0MiniBatchCount::Eight,
];
// Each pair is (credits, USD cents rounded up), for batches 1 through 8.
// Pin literal totals so rate changes fail independently of scaling checks.
#[test]
fn fixed_five_second_batch_prices_without_video_reference() {
let cases = [
(KinoviSeedance2p0MiniOutputResolution::FourEightyP, KinoviPricingTier::Consumer, [
(37.5, 20), (75.0, 39), (112.5, 59), (150.0, 78),
(187.5, 98), (225.0, 117), (262.5, 137), (300.0, 156),
]),
(KinoviSeedance2p0MiniOutputResolution::SevenTwentyP, KinoviPricingTier::Consumer, [
(100.0, 52), (200.0, 104), (300.0, 156), (400.0, 208),
(500.0, 260), (600.0, 311), (700.0, 363), (800.0, 415),
]),
(KinoviSeedance2p0MiniOutputResolution::FourEightyP, KinoviPricingTier::Enterprise, [
(15.0, 7), (30.0, 13), (45.0, 19), (60.0, 25),
(75.0, 31), (90.0, 38), (105.0, 44), (120.0, 50),
]),
(KinoviSeedance2p0MiniOutputResolution::SevenTwentyP, KinoviPricingTier::Enterprise, [
(40.0, 17), (80.0, 33), (120.0, 50), (160.0, 66),
(200.0, 83), (240.0, 99), (280.0, 116), (320.0, 132),
]),
];
for (resolution, tier, prices) in cases {
for (batch, (credits, usd_cents)) in BATCHES.into_iter().zip(prices) {
let request = build_request(5, Some(resolution), Some(batch));
let cost = request.calculate_costs(tier).total_cost;
assert_eq!(
(cost.kinovi_credits, cost.usd_cents_rounded_up),
(credits, usd_cents),
"{resolution:?} {tier:?} {batch:?}",
);
}
}
}
#[test]
fn batch_scales_total_linearly_at_both_tiers() {
let by_count = [
(KinoviSeedance2p0MiniBatchCount::One, 1.0),
(KinoviSeedance2p0MiniBatchCount::Two, 2.0),
(KinoviSeedance2p0MiniBatchCount::Three, 3.0),
(KinoviSeedance2p0MiniBatchCount::Four, 4.0),
(KinoviSeedance2p0MiniBatchCount::Five, 5.0),
(KinoviSeedance2p0MiniBatchCount::Six, 6.0),
(KinoviSeedance2p0MiniBatchCount::Seven, 7.0),
(KinoviSeedance2p0MiniBatchCount::Eight, 8.0),
fn fixed_five_second_batch_prices_with_video_reference() {
let cases = [
(KinoviSeedance2p0MiniOutputResolution::FourEightyP, KinoviPricingTier::Consumer, [
(47.5, 25), (95.0, 50), (142.5, 74), (190.0, 99),
(237.5, 124), (285.0, 148), (332.5, 173), (380.0, 197),
]),
(KinoviSeedance2p0MiniOutputResolution::SevenTwentyP, KinoviPricingTier::Consumer, [
(120.0, 63), (240.0, 125), (360.0, 187), (480.0, 249),
(600.0, 311), (720.0, 374), (840.0, 436), (960.0, 498),
]),
(KinoviSeedance2p0MiniOutputResolution::FourEightyP, KinoviPricingTier::Enterprise, [
(19.0, 8), (38.0, 16), (57.0, 24), (76.0, 32),
(95.0, 40), (114.0, 47), (133.0, 55), (152.0, 63),
]),
(KinoviSeedance2p0MiniOutputResolution::SevenTwentyP, KinoviPricingTier::Enterprise, [
(48.0, 20), (96.0, 40), (144.0, 60), (192.0, 79),
(240.0, 99), (288.0, 119), (336.0, 139), (384.0, 158),
]),
];
for (resolution, tier, prices) in cases {
for (batch, (credits, usd_cents)) in BATCHES.into_iter().zip(prices) {
let request = build_request(5, Some(resolution), Some(batch));
let request = with_video_ref(request);
let cost = request.calculate_costs(tier).total_cost;
assert_eq!(
(cost.kinovi_credits, cost.usd_cents_rounded_up),
(credits, usd_cents),
"{resolution:?} {tier:?} {batch:?}",
);
}
}
}
#[test]
fn all_batches_scale_every_resolution_and_surcharge_at_both_tiers() {
let batches = [
(None, 1),
(Some(KinoviSeedance2p0MiniBatchCount::One), 1),
(Some(KinoviSeedance2p0MiniBatchCount::Two), 2),
(Some(KinoviSeedance2p0MiniBatchCount::Three), 3),
(Some(KinoviSeedance2p0MiniBatchCount::Four), 4),
(Some(KinoviSeedance2p0MiniBatchCount::Five), 5),
(Some(KinoviSeedance2p0MiniBatchCount::Six), 6),
(Some(KinoviSeedance2p0MiniBatchCount::Seven), 7),
(Some(KinoviSeedance2p0MiniBatchCount::Eight), 8),
];
for tier in [KinoviPricingTier::Enterprise, KinoviPricingTier::Consumer] {
let base = r720(5).calculate_costs(tier).total_cost.kinovi_credits;
for (count, multiplier) in by_count {
let credits = build_request(5, None, Some(count)).calculate_costs(tier).total_cost.kinovi_credits;
assert_eq!(credits, base * multiplier, "batch {count:?} at {tier:?}");
for resolution in [
None,
Some(KinoviSeedance2p0MiniOutputResolution::FourEightyP),
Some(KinoviSeedance2p0MiniOutputResolution::SevenTwentyP),
] {
for duration in [4, 5, 15] {
for references in [
None,
Some(vec![]),
Some(vec!["https://example.com/ref.mp4".to_string()]),
Some(vec!["https://example.com/ref1.mp4".to_string(), "https://example.com/ref2.mp4".to_string()]),
] {
let mut request = build_request(duration, resolution, None);
request.reference_video_urls = references;
let single = request.calculate_costs(tier);
for (batch, count) in batches {
request.batch_count = batch;
let batched = request.calculate_costs(tier);
let context = format!("{tier:?} {resolution:?} {duration}s {batch:?} references={:?}", request.reference_video_urls);
assert_batch_cost(single.base_cost, batched.base_cost, count, tier, &context);
assert_batch_cost(single.total_cost, batched.total_cost, count, tier, &context);
match (single.video_reference_surcharge_cost, batched.video_reference_surcharge_cost) {
(None, None) => {},
(Some(single), Some(batched)) => assert_batch_cost(single, batched, count, tier, &context),
_ => panic!("{context}: batch count changed surcharge presence"),
}
}
}
}
}
}
}
@@ -48,6 +48,7 @@ pub struct GenerateSeedance2p5Request {
pub output_resolution: Option<KinoviSeedance2p5OutputResolution>,
pub duration_seconds: u8,
pub batch_count: Option<KinoviSeedance2p5BatchCount>,
/// CALCULATION-ONLY (never sent on the wire): the total seconds of
/// reference video input, summed across all reference videos. When video
@@ -137,6 +138,19 @@ pub enum KinoviSeedance2p5OutputFormat {
Mov,
}
/// Number of videos to generate in one request (1–8).
#[derive(Debug, Clone, Copy)]
pub enum KinoviSeedance2p5BatchCount {
One,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
}
// ── Pricing ──
//
// Seedance 2.5 credit pricing (credits per billed second). Without video
@@ -233,7 +247,18 @@ impl KinoviCostCalculatorTrait for GenerateSeedance2p5Request {
(rate, u64::from(self.duration_seconds))
};
let total_credits = rate.credits(tier) * billed_seconds as f64;
let batch_multiplier: f64 = match self.batch_count {
None | Some(KinoviSeedance2p5BatchCount::One) => 1.0,
Some(KinoviSeedance2p5BatchCount::Two) => 2.0,
Some(KinoviSeedance2p5BatchCount::Three) => 3.0,
Some(KinoviSeedance2p5BatchCount::Four) => 4.0,
Some(KinoviSeedance2p5BatchCount::Five) => 5.0,
Some(KinoviSeedance2p5BatchCount::Six) => 6.0,
Some(KinoviSeedance2p5BatchCount::Seven) => 7.0,
Some(KinoviSeedance2p5BatchCount::Eight) => 8.0,
};
let total_credits = rate.credits(tier) * billed_seconds as f64 * batch_multiplier;
tier.cost_from_credits(total_credits)
}
}
@@ -308,7 +333,7 @@ fn to_raw_request(req: GenerateSeedance2p5Request) -> WorkflowRunTaskRequest {
aspect_ratio,
output_resolution: Some(map_output_resolution(req.output_resolution)),
duration_seconds: req.duration_seconds,
batch_count: KinoviBatchCountRaw::One,
batch_count: map_batch_count(req.batch_count),
start_frame_url,
end_frame_url,
reference_image_urls,
@@ -354,6 +379,19 @@ fn map_output_resolution(res: Option<KinoviSeedance2p5OutputResolution>) -> Kino
}
}
fn map_batch_count(bc: Option<KinoviSeedance2p5BatchCount>) -> KinoviBatchCountRaw {
match bc {
Some(KinoviSeedance2p5BatchCount::One) | None => KinoviBatchCountRaw::One,
Some(KinoviSeedance2p5BatchCount::Two) => KinoviBatchCountRaw::Two,
Some(KinoviSeedance2p5BatchCount::Three) => KinoviBatchCountRaw::Three,
Some(KinoviSeedance2p5BatchCount::Four) => KinoviBatchCountRaw::Four,
Some(KinoviSeedance2p5BatchCount::Five) => KinoviBatchCountRaw::Five,
Some(KinoviSeedance2p5BatchCount::Six) => KinoviBatchCountRaw::Six,
Some(KinoviSeedance2p5BatchCount::Seven) => KinoviBatchCountRaw::Seven,
Some(KinoviSeedance2p5BatchCount::Eight) => KinoviBatchCountRaw::Eight,
}
}
// ── Tests ──
#[cfg(test)]
@@ -691,10 +729,185 @@ mod tests {
}
}
mod batch_price_tests {
use super::*;
const BATCHES: [KinoviSeedance2p5BatchCount; 8] = [
KinoviSeedance2p5BatchCount::One,
KinoviSeedance2p5BatchCount::Two,
KinoviSeedance2p5BatchCount::Three,
KinoviSeedance2p5BatchCount::Four,
KinoviSeedance2p5BatchCount::Five,
KinoviSeedance2p5BatchCount::Six,
KinoviSeedance2p5BatchCount::Seven,
KinoviSeedance2p5BatchCount::Eight,
];
// Each pair is (credits, USD cents rounded up), for batches 1 through 8.
// Pin literal totals so rate changes fail independently of scaling checks.
// Video-reference cases bill 5s output + 10s input per video.
#[test]
fn fixed_five_second_batch_prices_without_video_reference() {
let cases = [
(KinoviSeedance2p5OutputResolution::FourEightyP, KinoviPricingTier::Consumer, [
(130.0, 68), (260.0, 135), (390.0, 203), (520.0, 270),
(650.0, 337), (780.0, 405), (910.0, 472), (1040.0, 539),
]),
(KinoviSeedance2p5OutputResolution::SevenTwentyP, KinoviPricingTier::Consumer, [
(295.0, 153), (590.0, 306), (885.0, 459), (1180.0, 612),
(1475.0, 765), (1770.0, 918), (2065.0, 1071), (2360.0, 1223),
]),
(KinoviSeedance2p5OutputResolution::TenEightyP, KinoviPricingTier::Consumer, [
(717.0, 372), (1434.0, 744), (2151.0, 1115), (2868.0, 1487),
(3585.0, 1858), (4302.0, 2230), (5019.0, 2601), (5736.0, 2973),
]),
(KinoviSeedance2p5OutputResolution::FourEightyP, KinoviPricingTier::Enterprise, [
(130.0, 54), (260.0, 107), (390.0, 161), (520.0, 214),
(650.0, 268), (780.0, 321), (910.0, 375), (1040.0, 428),
]),
(KinoviSeedance2p5OutputResolution::SevenTwentyP, KinoviPricingTier::Enterprise, [
(295.0, 122), (590.0, 243), (885.0, 364), (1180.0, 486),
(1475.0, 607), (1770.0, 728), (2065.0, 850), (2360.0, 971),
]),
(KinoviSeedance2p5OutputResolution::TenEightyP, KinoviPricingTier::Enterprise, [
(681.15, 281), (1362.30, 561), (2043.45, 841), (2724.60, 1121),
(3405.75, 1401), (4086.90, 1681), (4768.05, 1961), (5449.20, 2241),
]),
];
for (resolution, tier, prices) in cases {
for (batch, (credits, usd_cents)) in BATCHES.into_iter().zip(prices) {
let mut request = text_to_video_request(5, Some(resolution));
request.batch_count = Some(batch);
let cost = request.calculate_costs(tier);
assert_eq!(
(cost.kinovi_credits, cost.usd_cents_rounded_up),
(credits, usd_cents),
"{resolution:?} {tier:?} {batch:?}",
);
}
}
}
#[test]
fn fixed_five_second_batch_prices_with_video_reference() {
let cases = [
(KinoviSeedance2p5OutputResolution::FourEightyP, KinoviPricingTier::Consumer, [
(240.0, 125), (480.0, 249), (720.0, 374), (960.0, 498),
(1200.0, 622), (1440.0, 747), (1680.0, 871), (1920.0, 995),
]),
(KinoviSeedance2p5OutputResolution::SevenTwentyP, KinoviPricingTier::Consumer, [
(525.0, 273), (1050.0, 545), (1575.0, 817), (2100.0, 1089),
(2625.0, 1361), (3150.0, 1633), (3675.0, 1905), (4200.0, 2177),
]),
(KinoviSeedance2p5OutputResolution::TenEightyP, KinoviPricingTier::Consumer, [
(1285.2, 666), (2570.4, 1332), (3855.6, 1998), (5140.8, 2664),
(6426.0, 3330), (7711.2, 3996), (8996.4, 4662), (10281.6, 5328),
]),
(KinoviSeedance2p5OutputResolution::FourEightyP, KinoviPricingTier::Enterprise, [
(240.0, 99), (480.0, 198), (720.0, 297), (960.0, 395),
(1200.0, 494), (1440.0, 593), (1680.0, 691), (1920.0, 790),
]),
(KinoviSeedance2p5OutputResolution::SevenTwentyP, KinoviPricingTier::Enterprise, [
(525.0, 216), (1050.0, 432), (1575.0, 648), (2100.0, 864),
(2625.0, 1080), (3150.0, 1296), (3675.0, 1512), (4200.0, 1728),
]),
(KinoviSeedance2p5OutputResolution::TenEightyP, KinoviPricingTier::Enterprise, [
(1221.0, 503), (2442.0, 1005), (3663.0, 1507), (4884.0, 2009),
(6105.0, 2511), (7326.0, 3013), (8547.0, 3515), (9768.0, 4018),
]),
];
for (resolution, tier, prices) in cases {
for (batch, (credits, usd_cents)) in BATCHES.into_iter().zip(prices) {
let mut request = text_to_video_request(5, Some(resolution));
request.batch_count = Some(batch);
set_reference_urls(&mut request, None, Some(vec!["https://example.com/ref.mp4".to_string()]), None);
request.total_input_seconds = Some(10);
let cost = request.calculate_costs(tier);
assert_eq!(
(cost.kinovi_credits, cost.usd_cents_rounded_up),
(credits, usd_cents),
"{resolution:?} {tier:?} {batch:?}",
);
}
}
}
}
// ── Relative pricing ──
mod relative_tests {
use super::*;
use crate::test_utils::assert_batch_cost::assert_batch_cost;
#[test]
fn all_batches_scale_every_resolution_modality_and_input_duration() {
let batches = [
(None, 1),
(Some(KinoviSeedance2p5BatchCount::One), 1),
(Some(KinoviSeedance2p5BatchCount::Two), 2),
(Some(KinoviSeedance2p5BatchCount::Three), 3),
(Some(KinoviSeedance2p5BatchCount::Four), 4),
(Some(KinoviSeedance2p5BatchCount::Five), 5),
(Some(KinoviSeedance2p5BatchCount::Six), 6),
(Some(KinoviSeedance2p5BatchCount::Seven), 7),
(Some(KinoviSeedance2p5BatchCount::Eight), 8),
];
for tier in [KinoviPricingTier::Enterprise, KinoviPricingTier::Consumer] {
for resolution in [
None,
Some(KinoviSeedance2p5OutputResolution::FourEightyP),
Some(KinoviSeedance2p5OutputResolution::SevenTwentyP),
Some(KinoviSeedance2p5OutputResolution::TenEightyP),
] {
for duration in [4, 5, 30] {
let text_request = text_to_video_request(duration, resolution);
let mut keyframe = keyframe_request(duration);
keyframe.output_resolution = resolution;
let mut image_audio = text_request.clone();
set_reference_urls(
&mut image_audio,
Some(vec!["https://example.com/image.png".to_string()]),
None,
Some(vec!["https://example.com/audio.mp3".to_string()]),
);
let mut empty_videos = text_request.clone();
set_reference_urls(&mut empty_videos, None, Some(vec![]), None);
let mut requests = vec![
("text".to_string(), text_request.clone()),
("keyframe".to_string(), keyframe),
("image/audio".to_string(), image_audio),
("empty videos".to_string(), empty_videos),
];
// Include unknown/zero inputs, the minimum, ordinary input, the
// maximum, and values clamped at either end of the billed range.
for input_seconds in [None, Some(0), Some(1), Some(4), Some(10), Some(30), Some(31), Some(u8::MAX)] {
let mut request = text_request.clone();
set_reference_urls(&mut request, None, Some(vec!["https://example.com/ref.mp4".to_string()]), None);
request.total_input_seconds = input_seconds;
requests.push((format!("video input={input_seconds:?}"), request));
}
for (modality, mut request) in requests {
let single = request.calculate_costs(tier);
for (batch, count) in batches {
request.batch_count = batch;
let batched = request.calculate_costs(tier);
let context = format!("{tier:?} {resolution:?} {duration}s {batch:?} {modality}");
assert_batch_cost(single, batched, count, tier, &context);
}
}
}
}
}
}
#[test]
fn batch_multiplies_output_and_reference_input_costs() {
let mut request = video_ref_480(5, Some(10));
request.batch_count = Some(KinoviSeedance2p5BatchCount::Eight);
assert_eq!(request.calculate_consumer_costs().kinovi_credits, 1920.0);
assert_eq!(request.calculate_enterprise_costs().kinovi_credits, 1920.0);
assert!(matches!(to_raw_request(request).batch_count, KinoviBatchCountRaw::Eight));
}
#[test]
fn video_reference_rate_is_cheaper_per_second() {
@@ -769,6 +982,7 @@ mod tests {
output_resolution: Option<KinoviSeedance2p5OutputResolution>,
) -> GenerateSeedance2p5Request {
GenerateSeedance2p5Request {
batch_count: None,
prompt: String::new(),
modality: KinoviSeedance2p5Modality::Reference {
aspect_ratio: None,
@@ -800,6 +1014,7 @@ mod tests {
fn keyframe_request(duration: u8) -> GenerateSeedance2p5Request {
GenerateSeedance2p5Request {
batch_count: None,
prompt: String::new(),
modality: KinoviSeedance2p5Modality::Keyframe {
start_frame_url: "https://example.com/start.png".to_string(),
@@ -862,6 +1077,7 @@ mod tests {
#[test]
fn keyframe_maps_frames_and_no_references() {
let raw = to_raw_request(GenerateSeedance2p5Request {
batch_count: None,
prompt: "Car drives into the sunset".to_string(),
modality: KinoviSeedance2p5Modality::Keyframe {
start_frame_url: "https://example.com/start.png".to_string(),
@@ -888,6 +1104,7 @@ mod tests {
#[test]
fn reference_maps_references_and_no_frames() {
let raw = to_raw_request(GenerateSeedance2p5Request {
batch_count: None,
prompt: "The t-rex @image1 eats the banana".to_string(),
modality: KinoviSeedance2p5Modality::Reference {
aspect_ratio: Some(KinoviSeedance2p5AspectRatio::UltraWide21x9),
@@ -918,6 +1135,7 @@ mod tests {
#[test]
fn resolution_1080p_maps_to_raw_1080p() {
let raw = to_raw_request(GenerateSeedance2p5Request {
batch_count: None,
prompt: "A car driving on the beach".to_string(),
modality: KinoviSeedance2p5Modality::Reference {
aspect_ratio: None,
@@ -940,6 +1158,7 @@ mod tests {
#[test]
fn reference_aspect_ratio_defaults_to_16x9() {
let raw = to_raw_request(GenerateSeedance2p5Request {
batch_count: None,
prompt: "Lightning hits a building".to_string(),
modality: KinoviSeedance2p5Modality::Reference {
aspect_ratio: None,
@@ -972,6 +1191,7 @@ mod tests {
session: &session,
host_override: None,
request: GenerateSeedance2p5Request {
batch_count: None,
prompt: "Lightning hits a building".to_string(),
modality: KinoviSeedance2p5Modality::Reference {
aspect_ratio: Some(KinoviSeedance2p5AspectRatio::Landscape16x9),
@@ -1006,6 +1226,7 @@ mod tests {
session: &session,
host_override: None,
request: GenerateSeedance2p5Request {
batch_count: None,
prompt: "A car driving on the beach".to_string(),
modality: KinoviSeedance2p5Modality::Reference {
aspect_ratio: Some(KinoviSeedance2p5AspectRatio::Landscape16x9),
@@ -1038,6 +1259,7 @@ mod tests {
session: &session,
host_override: None,
request: GenerateSeedance2p5Request {
batch_count: None,
prompt: "Car drives into the sunset".to_string(),
modality: KinoviSeedance2p5Modality::Keyframe {
start_frame_url: "https://static.seedance2-pro.com/materials/20260807/1786128486168-a2bf6132.png".to_string(),
@@ -1068,6 +1290,7 @@ mod tests {
session: &session,
host_override: None,
request: GenerateSeedance2p5Request {
batch_count: None,
prompt: "Car drives into the sunset".to_string(),
modality: KinoviSeedance2p5Modality::Keyframe {
start_frame_url: "https://static.seedance2-pro.com/materials/20260807/1786128486168-a2bf6132.png".to_string(),
@@ -1098,6 +1321,7 @@ mod tests {
session: &session,
host_override: None,
request: GenerateSeedance2p5Request {
batch_count: None,
prompt: "The t-rex @image1 eats the banana @image2".to_string(),
modality: KinoviSeedance2p5Modality::Reference {
aspect_ratio: Some(KinoviSeedance2p5AspectRatio::UltraWide21x9),
@@ -28,6 +28,7 @@ pub struct GenerateSeedance2p5PreviewRequest {
pub aspect_ratio: Option<KinoviSeedance2p5PreviewAspectRatio>,
pub output_resolution: Option<KinoviSeedance2p5PreviewOutputResolution>,
pub duration_seconds: u8,
pub batch_count: Option<KinoviSeedance2p5PreviewBatchCount>,
/// Reference images, referenced in prompts as @image1, @image2, etc.
pub reference_image_urls: Option<Vec<String>>,
/// Reference videos, referenced in prompts as @video1, @video2, etc.
@@ -58,6 +59,19 @@ pub enum KinoviSeedance2p5PreviewOutputResolution {
SevenTwentyP,
}
/// Number of videos to generate in one request (1–8).
#[derive(Debug, Clone, Copy)]
pub enum KinoviSeedance2p5PreviewBatchCount {
One,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
}
// ── Pricing ──
//
// Seedance 2.5 Preview credit pricing (per second of output duration):
@@ -97,7 +111,18 @@ impl KinoviCostCalculatorTrait for GenerateSeedance2p5PreviewRequest {
Some(KinoviSeedance2p5PreviewOutputResolution::FourEightyP) => SEEDANCE_2P5_PREVIEW_480P,
Some(KinoviSeedance2p5PreviewOutputResolution::SevenTwentyP) | None => SEEDANCE_2P5_PREVIEW_720P,
};
let total_credits = rate.credits(tier) * f64::from(self.duration_seconds);
let batch_multiplier: f64 = match self.batch_count {
None | Some(KinoviSeedance2p5PreviewBatchCount::One) => 1.0,
Some(KinoviSeedance2p5PreviewBatchCount::Two) => 2.0,
Some(KinoviSeedance2p5PreviewBatchCount::Three) => 3.0,
Some(KinoviSeedance2p5PreviewBatchCount::Four) => 4.0,
Some(KinoviSeedance2p5PreviewBatchCount::Five) => 5.0,
Some(KinoviSeedance2p5PreviewBatchCount::Six) => 6.0,
Some(KinoviSeedance2p5PreviewBatchCount::Seven) => 7.0,
Some(KinoviSeedance2p5PreviewBatchCount::Eight) => 8.0,
};
let total_credits = rate.credits(tier) * f64::from(self.duration_seconds) * batch_multiplier;
tier.cost_from_credits(total_credits)
}
}
@@ -139,7 +164,7 @@ fn to_raw_request(req: GenerateSeedance2p5PreviewRequest) -> WorkflowRunTaskRequ
aspect_ratio: map_aspect_ratio(req.aspect_ratio),
output_resolution: Some(map_output_resolution(req.output_resolution)),
duration_seconds: req.duration_seconds,
batch_count: KinoviBatchCountRaw::One,
batch_count: map_batch_count(req.batch_count),
start_frame_url: None,
end_frame_url: None,
reference_image_urls: req.reference_image_urls,
@@ -174,6 +199,19 @@ fn map_output_resolution(res: Option<KinoviSeedance2p5PreviewOutputResolution>)
}
}
fn map_batch_count(bc: Option<KinoviSeedance2p5PreviewBatchCount>) -> KinoviBatchCountRaw {
match bc {
Some(KinoviSeedance2p5PreviewBatchCount::One) | None => KinoviBatchCountRaw::One,
Some(KinoviSeedance2p5PreviewBatchCount::Two) => KinoviBatchCountRaw::Two,
Some(KinoviSeedance2p5PreviewBatchCount::Three) => KinoviBatchCountRaw::Three,
Some(KinoviSeedance2p5PreviewBatchCount::Four) => KinoviBatchCountRaw::Four,
Some(KinoviSeedance2p5PreviewBatchCount::Five) => KinoviBatchCountRaw::Five,
Some(KinoviSeedance2p5PreviewBatchCount::Six) => KinoviBatchCountRaw::Six,
Some(KinoviSeedance2p5PreviewBatchCount::Seven) => KinoviBatchCountRaw::Seven,
Some(KinoviSeedance2p5PreviewBatchCount::Eight) => KinoviBatchCountRaw::Eight,
}
}
// ── Tests ──
#[cfg(test)]
@@ -507,6 +545,110 @@ mod tests {
}
}
mod batch_tests {
use super::*;
use crate::test_utils::assert_batch_cost::assert_batch_cost;
const BATCHES: [KinoviSeedance2p5PreviewBatchCount; 8] = [
KinoviSeedance2p5PreviewBatchCount::One,
KinoviSeedance2p5PreviewBatchCount::Two,
KinoviSeedance2p5PreviewBatchCount::Three,
KinoviSeedance2p5PreviewBatchCount::Four,
KinoviSeedance2p5PreviewBatchCount::Five,
KinoviSeedance2p5PreviewBatchCount::Six,
KinoviSeedance2p5PreviewBatchCount::Seven,
KinoviSeedance2p5PreviewBatchCount::Eight,
];
// Each pair is (credits, USD cents rounded up), for batches 1 through 8.
// Pin literal totals so rate changes fail independently of scaling checks.
#[test]
fn fixed_five_second_batch_prices_with_and_without_references() {
let cases = [
(KinoviSeedance2p5PreviewOutputResolution::FourEightyP, KinoviPricingTier::Consumer, [
(230.75, 120), (461.50, 240), (692.25, 359), (923.00, 479),
(1153.75, 598), (1384.50, 718), (1615.25, 838), (1846.00, 957),
]),
(KinoviSeedance2p5PreviewOutputResolution::SevenTwentyP, KinoviPricingTier::Consumer, [
(460.15, 239), (920.30, 477), (1380.45, 716), (1840.60, 954),
(2300.75, 1193), (2760.90, 1431), (3221.05, 1670), (3681.20, 1908),
]),
(KinoviSeedance2p5PreviewOutputResolution::FourEightyP, KinoviPricingTier::Enterprise, [
(210.65, 87), (421.30, 174), (631.95, 260), (842.60, 347),
(1053.25, 434), (1263.90, 520), (1474.55, 607), (1685.20, 694),
]),
(KinoviSeedance2p5PreviewOutputResolution::SevenTwentyP, KinoviPricingTier::Enterprise, [
(421.3, 174), (842.6, 347), (1263.9, 520), (1685.2, 694),
(2106.5, 867), (2527.8, 1040), (2949.1, 1213), (3370.4, 1387),
]),
];
for (resolution, tier, prices) in cases {
for has_references in [false, true] {
for (batch, (credits, usd_cents)) in BATCHES.into_iter().zip(prices) {
let mut request = build_request(5, Some(resolution));
request.batch_count = Some(batch);
if has_references {
request = with_all_reference_types(request);
}
let cost = request.calculate_costs(tier);
assert_eq!(
(cost.kinovi_credits, cost.usd_cents_rounded_up),
(credits, usd_cents),
"{resolution:?} {tier:?} {batch:?} references={has_references}",
);
}
}
}
}
#[test]
fn all_batches_scale_every_resolution_with_and_without_references() {
let batches = [
(None, 1),
(Some(KinoviSeedance2p5PreviewBatchCount::One), 1),
(Some(KinoviSeedance2p5PreviewBatchCount::Two), 2),
(Some(KinoviSeedance2p5PreviewBatchCount::Three), 3),
(Some(KinoviSeedance2p5PreviewBatchCount::Four), 4),
(Some(KinoviSeedance2p5PreviewBatchCount::Five), 5),
(Some(KinoviSeedance2p5PreviewBatchCount::Six), 6),
(Some(KinoviSeedance2p5PreviewBatchCount::Seven), 7),
(Some(KinoviSeedance2p5PreviewBatchCount::Eight), 8),
];
for tier in [KinoviPricingTier::Enterprise, KinoviPricingTier::Consumer] {
for resolution in [
None,
Some(KinoviSeedance2p5PreviewOutputResolution::FourEightyP),
Some(KinoviSeedance2p5PreviewOutputResolution::SevenTwentyP),
] {
for duration in [4, 5, 30] {
for has_references in [false, true] {
let mut request = build_request(duration, resolution);
if has_references {
request = with_all_reference_types(request);
}
let single = request.calculate_costs(tier);
for (batch, count) in batches {
request.batch_count = batch;
let batched = request.calculate_costs(tier);
let context = format!("{tier:?} {resolution:?} {duration}s {batch:?} references={has_references}");
assert_batch_cost(single, batched, count, tier, &context);
}
}
}
}
}
}
#[test]
fn batch_costs_and_request_count_agree() {
let mut request = r480(4);
request.batch_count = Some(KinoviSeedance2p5PreviewBatchCount::Eight);
assert_eq!(request.calculate_consumer_costs().kinovi_credits, 1476.8);
assert_eq!(request.calculate_enterprise_costs().kinovi_credits, 1348.16);
assert!(matches!(to_raw_request(request).batch_count, KinoviBatchCountRaw::Eight));
}
}
// ── Helpers ──
fn build_request(
@@ -514,6 +656,7 @@ mod tests {
output_resolution: Option<KinoviSeedance2p5PreviewOutputResolution>,
) -> GenerateSeedance2p5PreviewRequest {
GenerateSeedance2p5PreviewRequest {
batch_count: None,
prompt: String::new(),
aspect_ratio: None,
output_resolution,
@@ -556,6 +699,7 @@ mod tests {
session: &session,
host_override: None,
request: GenerateSeedance2p5PreviewRequest {
batch_count: None,
prompt: "A man is running from a t-rex".to_string(),
aspect_ratio: Some(KinoviSeedance2p5PreviewAspectRatio::Landscape16x9),
output_resolution: Some(KinoviSeedance2p5PreviewOutputResolution::FourEightyP),
@@ -582,6 +726,7 @@ mod tests {
session: &session,
host_override: None,
request: GenerateSeedance2p5PreviewRequest {
batch_count: None,
prompt: "A corgi and a shiba are playing chess against one another".to_string(),
aspect_ratio: None,
output_resolution: None,
@@ -0,0 +1,97 @@
//! Wan 3.0 image-to-video through the Kinovi consumer workflow endpoint.
use crate::creds::kinovi_web_session::KinoviWebSession;
use crate::error::kinovi_web_error::KinoviWebError;
use crate::generate::video::wan_3p0::{BUSINESS_TYPE, Wan3p0ApiParams, Wan3p0Model, invalid_field};
use crate::pricing::cost::kinovi_fractional_generation_cost::KinoviFractionalGenerationCost;
use crate::pricing::kinovi_cost_calculator_trait::KinoviCostCalculatorTrait;
use crate::pricing::kinovi_pricing_tier::KinoviPricingTier;
use crate::requests::kinovi_host::KinoviHost;
use crate::requests::workflow_run_task::workflow_run_task::{workflow_run_task_custom, WorkflowRunTaskCustomArgs};
pub use crate::generate::video::wan_3p0::{KinoviWan3p0AspectRatio, KinoviWan3p0OutputResolution};
pub use crate::requests::workflow_run_task::workflow_run_task::WorkflowRunTaskResponse as GenerateWan3p0ImageToVideoResponse;
pub struct GenerateWan3p0ImageToVideoArgs<'a> {
pub request: GenerateWan3p0ImageToVideoRequest,
pub session: &'a KinoviWebSession,
pub maybe_host_override: Option<KinoviHost>,
}
#[derive(Clone, Debug)]
pub struct GenerateWan3p0ImageToVideoRequest {
/// Up to 20,000 characters; may be empty when media is supplied.
pub prompt: String,
/// None resolves to 16:9.
pub maybe_aspect_ratio: Option<KinoviWan3p0AspectRatio>,
/// None resolves to 720p for both pricing and generation.
pub maybe_output_resolution: Option<KinoviWan3p0OutputResolution>,
/// Any whole-second duration from 2 through 30 (not just the UI presets).
pub duration_seconds: u8,
/// Optional noise seed, inclusive range 0..=2147483647.
pub maybe_seed: Option<u32>,
/// None keeps Kinovi's default generated soundtrack; false disables it.
pub maybe_generate_audio: Option<bool>,
/// Required for generation; optional so costs can be estimated without media.
pub maybe_start_frame_url: Option<String>,
/// Optional final keyframe. Requires a start frame.
pub maybe_end_frame_url: Option<String>,
}
impl Default for GenerateWan3p0ImageToVideoRequest {
fn default() -> Self {
Self {
prompt: String::new(),
maybe_aspect_ratio: None,
maybe_output_resolution: None,
duration_seconds: 5,
maybe_seed: None,
maybe_generate_audio: None,
maybe_start_frame_url: None,
maybe_end_frame_url: None,
}
}
}
impl KinoviCostCalculatorTrait for GenerateWan3p0ImageToVideoRequest {
type Cost = KinoviFractionalGenerationCost;
/// Prices output seconds only. Images, video/audio references, seed, and
/// aspect ratio do not change the credit charge. Both tiers use the same
/// credit rate and their respective credit-to-USD conversion.
fn calculate_costs(&self, tier: KinoviPricingTier) -> Self::Cost {
Wan3p0Model::ImageToVideo.calculate_costs(self.maybe_output_resolution, self.duration_seconds, tier)
}
}
impl GenerateWan3p0ImageToVideoRequest {
pub(super) fn build_api_params(self) -> Result<Wan3p0ApiParams, KinoviWebError> {
if self.maybe_end_frame_url.is_some() && self.maybe_start_frame_url.is_none() {
return Err(invalid_field("maybe_end_frame_url", "", "an end frame requires a start frame"));
}
let mut params = Wan3p0ApiParams::new(
Wan3p0Model::ImageToVideo,
self.prompt,
self.maybe_aspect_ratio,
self.maybe_output_resolution,
self.duration_seconds,
self.maybe_seed,
self.maybe_generate_audio,
)?;
params.uploaded_urls = Some(self.maybe_start_frame_url.into_iter().chain(self.maybe_end_frame_url).collect());
params.validate_media(self.duration_seconds, None)?;
Ok(params)
}
}
pub async fn generate_wan_3p0_image_to_video(
args: GenerateWan3p0ImageToVideoArgs<'_>,
) -> Result<GenerateWan3p0ImageToVideoResponse, KinoviWebError> {
workflow_run_task_custom(WorkflowRunTaskCustomArgs {
business_type: BUSINESS_TYPE,
api_params: args.request.build_api_params()?,
session: args.session,
host_override: args.maybe_host_override,
})
.await
}
@@ -0,0 +1,97 @@
//! Wan 3.0 Prime image-to-video through the Kinovi consumer workflow endpoint.
use crate::creds::kinovi_web_session::KinoviWebSession;
use crate::error::kinovi_web_error::KinoviWebError;
use crate::generate::video::wan_3p0::{BUSINESS_TYPE, Wan3p0ApiParams, Wan3p0Model, invalid_field};
use crate::pricing::cost::kinovi_fractional_generation_cost::KinoviFractionalGenerationCost;
use crate::pricing::kinovi_cost_calculator_trait::KinoviCostCalculatorTrait;
use crate::pricing::kinovi_pricing_tier::KinoviPricingTier;
use crate::requests::kinovi_host::KinoviHost;
use crate::requests::workflow_run_task::workflow_run_task::{workflow_run_task_custom, WorkflowRunTaskCustomArgs};
pub use crate::generate::video::wan_3p0::{KinoviWan3p0AspectRatio, KinoviWan3p0OutputResolution};
pub use crate::requests::workflow_run_task::workflow_run_task::WorkflowRunTaskResponse as GenerateWan3p0PrimeImageToVideoResponse;
pub struct GenerateWan3p0PrimeImageToVideoArgs<'a> {
pub request: GenerateWan3p0PrimeImageToVideoRequest,
pub session: &'a KinoviWebSession,
pub maybe_host_override: Option<KinoviHost>,
}
#[derive(Clone, Debug)]
pub struct GenerateWan3p0PrimeImageToVideoRequest {
/// Up to 20,000 characters; may be empty when media is supplied.
pub prompt: String,
/// None resolves to 16:9.
pub maybe_aspect_ratio: Option<KinoviWan3p0AspectRatio>,
/// None resolves to 720p for both pricing and generation.
pub maybe_output_resolution: Option<KinoviWan3p0OutputResolution>,
/// Any whole-second duration from 2 through 30 (not just the UI presets).
pub duration_seconds: u8,
/// Optional noise seed, inclusive range 0..=2147483647.
pub maybe_seed: Option<u32>,
/// None keeps Kinovi's default generated soundtrack; false disables it.
pub maybe_generate_audio: Option<bool>,
/// Required for generation; optional so costs can be estimated without media.
pub maybe_start_frame_url: Option<String>,
/// Optional final keyframe. Requires a start frame.
pub maybe_end_frame_url: Option<String>,
}
impl Default for GenerateWan3p0PrimeImageToVideoRequest {
fn default() -> Self {
Self {
prompt: String::new(),
maybe_aspect_ratio: None,
maybe_output_resolution: None,
duration_seconds: 5,
maybe_seed: None,
maybe_generate_audio: None,
maybe_start_frame_url: None,
maybe_end_frame_url: None,
}
}
}
impl KinoviCostCalculatorTrait for GenerateWan3p0PrimeImageToVideoRequest {
type Cost = KinoviFractionalGenerationCost;
/// Prices output seconds only. Images, video/audio references, seed, and
/// aspect ratio do not change the credit charge. Both tiers use the same
/// credit rate and their respective credit-to-USD conversion.
fn calculate_costs(&self, tier: KinoviPricingTier) -> Self::Cost {
Wan3p0Model::PrimeImageToVideo.calculate_costs(self.maybe_output_resolution, self.duration_seconds, tier)
}
}
impl GenerateWan3p0PrimeImageToVideoRequest {
pub(super) fn build_api_params(self) -> Result<Wan3p0ApiParams, KinoviWebError> {
if self.maybe_end_frame_url.is_some() && self.maybe_start_frame_url.is_none() {
return Err(invalid_field("maybe_end_frame_url", "", "an end frame requires a start frame"));
}
let mut params = Wan3p0ApiParams::new(
Wan3p0Model::PrimeImageToVideo,
self.prompt,
self.maybe_aspect_ratio,
self.maybe_output_resolution,
self.duration_seconds,
self.maybe_seed,
self.maybe_generate_audio,
)?;
params.uploaded_urls = Some(self.maybe_start_frame_url.into_iter().chain(self.maybe_end_frame_url).collect());
params.validate_media(self.duration_seconds, None)?;
Ok(params)
}
}
pub async fn generate_wan_3p0_prime_image_to_video(
args: GenerateWan3p0PrimeImageToVideoArgs<'_>,
) -> Result<GenerateWan3p0PrimeImageToVideoResponse, KinoviWebError> {
workflow_run_task_custom(WorkflowRunTaskCustomArgs {
business_type: BUSINESS_TYPE,
api_params: args.request.build_api_params()?,
session: args.session,
host_override: args.maybe_host_override,
})
.await
}
@@ -0,0 +1,105 @@
//! Wan 3.0 Prime ref-to-video through the Kinovi consumer workflow endpoint.
use crate::creds::kinovi_web_session::KinoviWebSession;
use crate::error::kinovi_web_error::KinoviWebError;
use crate::generate::video::wan_3p0::{BUSINESS_TYPE, Wan3p0ApiParams, Wan3p0Model};
use crate::pricing::cost::kinovi_fractional_generation_cost::KinoviFractionalGenerationCost;
use crate::pricing::kinovi_cost_calculator_trait::KinoviCostCalculatorTrait;
use crate::pricing::kinovi_pricing_tier::KinoviPricingTier;
use crate::requests::kinovi_host::KinoviHost;
use crate::requests::workflow_run_task::workflow_run_task::{workflow_run_task_custom, WorkflowRunTaskCustomArgs};
pub use crate::generate::video::wan_3p0::{KinoviWan3p0AspectRatio, KinoviWan3p0OutputResolution};
pub use crate::requests::workflow_run_task::workflow_run_task::WorkflowRunTaskResponse as GenerateWan3p0PrimeRefToVideoResponse;
pub struct GenerateWan3p0PrimeRefToVideoArgs<'a> {
pub request: GenerateWan3p0PrimeRefToVideoRequest,
pub session: &'a KinoviWebSession,
pub maybe_host_override: Option<KinoviHost>,
}
#[derive(Clone, Debug)]
pub struct GenerateWan3p0PrimeRefToVideoRequest {
/// Up to 20,000 characters; may be empty when media is supplied.
pub prompt: String,
/// None resolves to 16:9.
pub maybe_aspect_ratio: Option<KinoviWan3p0AspectRatio>,
/// None resolves to 720p for both pricing and generation.
pub maybe_output_resolution: Option<KinoviWan3p0OutputResolution>,
/// Any whole-second duration from 2 through 30 (not just the UI presets).
pub duration_seconds: u8,
/// Optional noise seed, inclusive range 0..=2147483647.
pub maybe_seed: Option<u32>,
/// None keeps Kinovi's default generated soundtrack; false disables it.
pub maybe_generate_audio: Option<bool>,
/// Up to 10 reference images, addressed as Image 1, Image 2, etc.
pub maybe_reference_image_urls: Option<Vec<String>>,
/// Up to 5 videos, each 1-15s, totaling at most 15s.
pub maybe_reference_video_urls: Option<Vec<String>>,
/// Up to 5 audio files, each 1-15s, totaling at most 15s.
/// Actual file durations are validated by Kinovi.
pub maybe_reference_audio_urls: Option<Vec<String>>,
/// Optional measured TOTAL reference-video duration, used only for validation.
/// Reference duration + output duration must not exceed 30s. This metadata
/// is not sent or billed. Without it, Kinovi validates the actual durations.
pub maybe_total_reference_video_duration_millis: Option<u32>,
}
impl Default for GenerateWan3p0PrimeRefToVideoRequest {
fn default() -> Self {
Self {
prompt: String::new(),
maybe_aspect_ratio: None,
maybe_output_resolution: None,
duration_seconds: 5,
maybe_seed: None,
maybe_generate_audio: None,
maybe_reference_image_urls: None,
maybe_reference_video_urls: None,
maybe_reference_audio_urls: None,
maybe_total_reference_video_duration_millis: None,
}
}
}
impl KinoviCostCalculatorTrait for GenerateWan3p0PrimeRefToVideoRequest {
type Cost = KinoviFractionalGenerationCost;
/// Prices output seconds only. Images, video/audio references, seed, and
/// aspect ratio do not change the credit charge. Both tiers use the same
/// credit rate and their respective credit-to-USD conversion.
fn calculate_costs(&self, tier: KinoviPricingTier) -> Self::Cost {
Wan3p0Model::PrimeRefToVideo.calculate_costs(self.maybe_output_resolution, self.duration_seconds, tier)
}
}
impl GenerateWan3p0PrimeRefToVideoRequest {
pub(super) fn build_api_params(self) -> Result<Wan3p0ApiParams, KinoviWebError> {
let mut params = Wan3p0ApiParams::new(
Wan3p0Model::PrimeRefToVideo,
self.prompt,
self.maybe_aspect_ratio,
self.maybe_output_resolution,
self.duration_seconds,
self.maybe_seed,
self.maybe_generate_audio,
)?;
params.uploaded_urls = self.maybe_reference_image_urls;
params.video_urls = self.maybe_reference_video_urls;
params.audio_urls = self.maybe_reference_audio_urls;
params.validate_media(self.duration_seconds, self.maybe_total_reference_video_duration_millis)?;
Ok(params)
}
}
pub async fn generate_wan_3p0_prime_ref_to_video(
args: GenerateWan3p0PrimeRefToVideoArgs<'_>,
) -> Result<GenerateWan3p0PrimeRefToVideoResponse, KinoviWebError> {
workflow_run_task_custom(WorkflowRunTaskCustomArgs {
business_type: BUSINESS_TYPE,
api_params: args.request.build_api_params()?,
session: args.session,
host_override: args.maybe_host_override,
})
.await
}
@@ -0,0 +1,87 @@
//! Wan 3.0 Prime text-to-video through the Kinovi consumer workflow endpoint.
use crate::creds::kinovi_web_session::KinoviWebSession;
use crate::error::kinovi_web_error::KinoviWebError;
use crate::generate::video::wan_3p0::{BUSINESS_TYPE, Wan3p0ApiParams, Wan3p0Model};
use crate::pricing::cost::kinovi_fractional_generation_cost::KinoviFractionalGenerationCost;
use crate::pricing::kinovi_cost_calculator_trait::KinoviCostCalculatorTrait;
use crate::pricing::kinovi_pricing_tier::KinoviPricingTier;
use crate::requests::kinovi_host::KinoviHost;
use crate::requests::workflow_run_task::workflow_run_task::{workflow_run_task_custom, WorkflowRunTaskCustomArgs};
pub use crate::generate::video::wan_3p0::{KinoviWan3p0AspectRatio, KinoviWan3p0OutputResolution};
pub use crate::requests::workflow_run_task::workflow_run_task::WorkflowRunTaskResponse as GenerateWan3p0PrimeTextToVideoResponse;
pub struct GenerateWan3p0PrimeTextToVideoArgs<'a> {
pub request: GenerateWan3p0PrimeTextToVideoRequest,
pub session: &'a KinoviWebSession,
pub maybe_host_override: Option<KinoviHost>,
}
#[derive(Clone, Debug)]
pub struct GenerateWan3p0PrimeTextToVideoRequest {
/// Required prompt, up to 20,000 characters.
pub prompt: String,
/// None resolves to 16:9.
pub maybe_aspect_ratio: Option<KinoviWan3p0AspectRatio>,
/// None resolves to 720p for both pricing and generation.
pub maybe_output_resolution: Option<KinoviWan3p0OutputResolution>,
/// Any whole-second duration from 2 through 30 (not just the UI presets).
pub duration_seconds: u8,
/// Optional noise seed, inclusive range 0..=2147483647.
pub maybe_seed: Option<u32>,
/// None keeps Kinovi's default generated soundtrack; false disables it.
pub maybe_generate_audio: Option<bool>,
}
impl Default for GenerateWan3p0PrimeTextToVideoRequest {
fn default() -> Self {
Self {
prompt: String::new(),
maybe_aspect_ratio: None,
maybe_output_resolution: None,
duration_seconds: 5,
maybe_seed: None,
maybe_generate_audio: None,
}
}
}
impl KinoviCostCalculatorTrait for GenerateWan3p0PrimeTextToVideoRequest {
type Cost = KinoviFractionalGenerationCost;
/// Prices output seconds only. Images, video/audio references, seed, and
/// aspect ratio do not change the credit charge. Both tiers use the same
/// credit rate and their respective credit-to-USD conversion.
fn calculate_costs(&self, tier: KinoviPricingTier) -> Self::Cost {
Wan3p0Model::PrimeTextToVideo.calculate_costs(self.maybe_output_resolution, self.duration_seconds, tier)
}
}
impl GenerateWan3p0PrimeTextToVideoRequest {
pub(super) fn build_api_params(self) -> Result<Wan3p0ApiParams, KinoviWebError> {
let mut params = Wan3p0ApiParams::new(
Wan3p0Model::PrimeTextToVideo,
self.prompt,
self.maybe_aspect_ratio,
self.maybe_output_resolution,
self.duration_seconds,
self.maybe_seed,
self.maybe_generate_audio,
)?;
params.validate_media(self.duration_seconds, None)?;
Ok(params)
}
}
pub async fn generate_wan_3p0_prime_text_to_video(
args: GenerateWan3p0PrimeTextToVideoArgs<'_>,
) -> Result<GenerateWan3p0PrimeTextToVideoResponse, KinoviWebError> {
workflow_run_task_custom(WorkflowRunTaskCustomArgs {
business_type: BUSINESS_TYPE,
api_params: args.request.build_api_params()?,
session: args.session,
host_override: args.maybe_host_override,
})
.await
}
@@ -0,0 +1,105 @@
//! Wan 3.0 ref-to-video through the Kinovi consumer workflow endpoint.
use crate::creds::kinovi_web_session::KinoviWebSession;
use crate::error::kinovi_web_error::KinoviWebError;
use crate::generate::video::wan_3p0::{BUSINESS_TYPE, Wan3p0ApiParams, Wan3p0Model};
use crate::pricing::cost::kinovi_fractional_generation_cost::KinoviFractionalGenerationCost;
use crate::pricing::kinovi_cost_calculator_trait::KinoviCostCalculatorTrait;
use crate::pricing::kinovi_pricing_tier::KinoviPricingTier;
use crate::requests::kinovi_host::KinoviHost;
use crate::requests::workflow_run_task::workflow_run_task::{workflow_run_task_custom, WorkflowRunTaskCustomArgs};
pub use crate::generate::video::wan_3p0::{KinoviWan3p0AspectRatio, KinoviWan3p0OutputResolution};
pub use crate::requests::workflow_run_task::workflow_run_task::WorkflowRunTaskResponse as GenerateWan3p0RefToVideoResponse;
pub struct GenerateWan3p0RefToVideoArgs<'a> {
pub request: GenerateWan3p0RefToVideoRequest,
pub session: &'a KinoviWebSession,
pub maybe_host_override: Option<KinoviHost>,
}
#[derive(Clone, Debug)]
pub struct GenerateWan3p0RefToVideoRequest {
/// Up to 20,000 characters; may be empty when media is supplied.
pub prompt: String,
/// None resolves to 16:9.
pub maybe_aspect_ratio: Option<KinoviWan3p0AspectRatio>,
/// None resolves to 720p for both pricing and generation.
pub maybe_output_resolution: Option<KinoviWan3p0OutputResolution>,
/// Any whole-second duration from 2 through 30 (not just the UI presets).
pub duration_seconds: u8,
/// Optional noise seed, inclusive range 0..=2147483647.
pub maybe_seed: Option<u32>,
/// None keeps Kinovi's default generated soundtrack; false disables it.
pub maybe_generate_audio: Option<bool>,
/// Up to 10 reference images, addressed as Image 1, Image 2, etc.
pub maybe_reference_image_urls: Option<Vec<String>>,
/// Up to 5 videos, each 1-15s, totaling at most 15s.
pub maybe_reference_video_urls: Option<Vec<String>>,
/// Up to 5 audio files, each 1-15s, totaling at most 15s.
/// Actual file durations are validated by Kinovi.
pub maybe_reference_audio_urls: Option<Vec<String>>,
/// Optional measured TOTAL reference-video duration, used only for validation.
/// Reference duration + output duration must not exceed 30s. This metadata
/// is not sent or billed. Without it, Kinovi validates the actual durations.
pub maybe_total_reference_video_duration_millis: Option<u32>,
}
impl Default for GenerateWan3p0RefToVideoRequest {
fn default() -> Self {
Self {
prompt: String::new(),
maybe_aspect_ratio: None,
maybe_output_resolution: None,
duration_seconds: 5,
maybe_seed: None,
maybe_generate_audio: None,
maybe_reference_image_urls: None,
maybe_reference_video_urls: None,
maybe_reference_audio_urls: None,
maybe_total_reference_video_duration_millis: None,
}
}
}
impl KinoviCostCalculatorTrait for GenerateWan3p0RefToVideoRequest {
type Cost = KinoviFractionalGenerationCost;
/// Prices output seconds only. Images, video/audio references, seed, and
/// aspect ratio do not change the credit charge. Both tiers use the same
/// credit rate and their respective credit-to-USD conversion.
fn calculate_costs(&self, tier: KinoviPricingTier) -> Self::Cost {
Wan3p0Model::RefToVideo.calculate_costs(self.maybe_output_resolution, self.duration_seconds, tier)
}
}
impl GenerateWan3p0RefToVideoRequest {
pub(super) fn build_api_params(self) -> Result<Wan3p0ApiParams, KinoviWebError> {
let mut params = Wan3p0ApiParams::new(
Wan3p0Model::RefToVideo,
self.prompt,
self.maybe_aspect_ratio,
self.maybe_output_resolution,
self.duration_seconds,
self.maybe_seed,
self.maybe_generate_audio,
)?;
params.uploaded_urls = self.maybe_reference_image_urls;
params.video_urls = self.maybe_reference_video_urls;
params.audio_urls = self.maybe_reference_audio_urls;
params.validate_media(self.duration_seconds, self.maybe_total_reference_video_duration_millis)?;
Ok(params)
}
}
pub async fn generate_wan_3p0_ref_to_video(
args: GenerateWan3p0RefToVideoArgs<'_>,
) -> Result<GenerateWan3p0RefToVideoResponse, KinoviWebError> {
workflow_run_task_custom(WorkflowRunTaskCustomArgs {
business_type: BUSINESS_TYPE,
api_params: args.request.build_api_params()?,
session: args.session,
host_override: args.maybe_host_override,
})
.await
}
@@ -0,0 +1,87 @@
//! Wan 3.0 text-to-video through the Kinovi consumer workflow endpoint.
use crate::creds::kinovi_web_session::KinoviWebSession;
use crate::error::kinovi_web_error::KinoviWebError;
use crate::generate::video::wan_3p0::{BUSINESS_TYPE, Wan3p0ApiParams, Wan3p0Model};
use crate::pricing::cost::kinovi_fractional_generation_cost::KinoviFractionalGenerationCost;
use crate::pricing::kinovi_cost_calculator_trait::KinoviCostCalculatorTrait;
use crate::pricing::kinovi_pricing_tier::KinoviPricingTier;
use crate::requests::kinovi_host::KinoviHost;
use crate::requests::workflow_run_task::workflow_run_task::{workflow_run_task_custom, WorkflowRunTaskCustomArgs};
pub use crate::generate::video::wan_3p0::{KinoviWan3p0AspectRatio, KinoviWan3p0OutputResolution};
pub use crate::requests::workflow_run_task::workflow_run_task::WorkflowRunTaskResponse as GenerateWan3p0TextToVideoResponse;
pub struct GenerateWan3p0TextToVideoArgs<'a> {
pub request: GenerateWan3p0TextToVideoRequest,
pub session: &'a KinoviWebSession,
pub maybe_host_override: Option<KinoviHost>,
}
#[derive(Clone, Debug)]
pub struct GenerateWan3p0TextToVideoRequest {
/// Required prompt, up to 20,000 characters.
pub prompt: String,
/// None resolves to 16:9.
pub maybe_aspect_ratio: Option<KinoviWan3p0AspectRatio>,
/// None resolves to 720p for both pricing and generation.
pub maybe_output_resolution: Option<KinoviWan3p0OutputResolution>,
/// Any whole-second duration from 2 through 30 (not just the UI presets).
pub duration_seconds: u8,
/// Optional noise seed, inclusive range 0..=2147483647.
pub maybe_seed: Option<u32>,
/// None keeps Kinovi's default generated soundtrack; false disables it.
pub maybe_generate_audio: Option<bool>,
}
impl Default for GenerateWan3p0TextToVideoRequest {
fn default() -> Self {
Self {
prompt: String::new(),
maybe_aspect_ratio: None,
maybe_output_resolution: None,
duration_seconds: 5,
maybe_seed: None,
maybe_generate_audio: None,
}
}
}
impl KinoviCostCalculatorTrait for GenerateWan3p0TextToVideoRequest {
type Cost = KinoviFractionalGenerationCost;
/// Prices output seconds only. Images, video/audio references, seed, and
/// aspect ratio do not change the credit charge. Both tiers use the same
/// credit rate and their respective credit-to-USD conversion.
fn calculate_costs(&self, tier: KinoviPricingTier) -> Self::Cost {
Wan3p0Model::TextToVideo.calculate_costs(self.maybe_output_resolution, self.duration_seconds, tier)
}
}
impl GenerateWan3p0TextToVideoRequest {
pub(super) fn build_api_params(self) -> Result<Wan3p0ApiParams, KinoviWebError> {
let mut params = Wan3p0ApiParams::new(
Wan3p0Model::TextToVideo,
self.prompt,
self.maybe_aspect_ratio,
self.maybe_output_resolution,
self.duration_seconds,
self.maybe_seed,
self.maybe_generate_audio,
)?;
params.validate_media(self.duration_seconds, None)?;
Ok(params)
}
}
pub async fn generate_wan_3p0_text_to_video(
args: GenerateWan3p0TextToVideoArgs<'_>,
) -> Result<GenerateWan3p0TextToVideoResponse, KinoviWebError> {
workflow_run_task_custom(WorkflowRunTaskCustomArgs {
business_type: BUSINESS_TYPE,
api_params: args.request.build_api_params()?,
session: args.session,
host_override: args.maybe_host_override,
})
.await
}
@@ -4,3 +4,13 @@ pub mod generate_seedance_2p0_fast;
pub mod generate_seedance_2p0_mini;
pub mod generate_seedance_2p5;
pub mod generate_seedance_2p5_preview;
pub mod generate_wan_3p0_text_to_video;
pub mod generate_wan_3p0_image_to_video;
pub mod generate_wan_3p0_ref_to_video;
pub mod generate_wan_3p0_prime_text_to_video;
pub mod generate_wan_3p0_prime_image_to_video;
pub mod generate_wan_3p0_prime_ref_to_video;
pub mod wan_3p0;
#[cfg(test)]
mod wan_3p0_tests;
@@ -0,0 +1,245 @@
//! Shared options and wire format for Wan 3.0 and Wan 3.0 Prime.
//!
//! Schema: https://kinovi.ai/models/wan3-ref-to-video (checked 2026-09-18).
//! All modes bill only OUTPUT seconds; reference media does not add a surcharge.
//!
//! Output accepts any whole-second duration from 2 through 30. Reference
//! videos must each be 1-15s and total at most 15s; their total plus output
//! duration must not exceed 30s. Thus 10s of video references allows at most
//! 20s of output. Audio and image references do not consume this video budget.
//! The optional measured reference duration enables local validation; otherwise
//! Kinovi checks actual file durations. This client does not probe media URLs.
//!
//! Pricing sources (all three modalities share each family's rates):
//! - https://kinovi.ai/models/wan3-text-to-video
//! - https://kinovi.ai/models/wan3-prime-ref-to-video
//! Pricing maintenance and the September 23 promotion: `costs_wan_3p0.md`
//! at the crate root.
use serde_derive::Serialize;
use crate::error::kinovi_web_client_error::KinoviWebClientError;
use crate::error::kinovi_web_error::KinoviWebError;
use crate::pricing::cost::kinovi_fractional_generation_cost::KinoviFractionalGenerationCost;
use crate::pricing::kinovi_pricing_tier::KinoviPricingTier;
pub(super) const BUSINESS_TYPE: &str = "wan3-video-generation";
pub const MAX_SEED: u32 = 2_147_483_647;
pub const MIN_DURATION_SECONDS: u8 = 2;
pub const MAX_DURATION_SECONDS: u8 = 30;
pub const MAX_REFERENCE_VIDEO_DURATION_MILLIS: u32 = 15_000;
// Hundredths of a credit per OUTPUT second, as observed 2026-09-18.
// Regular Wan intentionally uses the promotional rates (30% off until 2026-09-23).
// TODO(2026-09-24): Recheck live pricing using costs_wan_3p0.md at the crate root.
// That note records advertised replacement rates, precision needs, and test updates.
// Keep these promotional rates until a reviewed update; no automatic date switch.
// Prime 480p is 18.86/s, so 30s costs 565.8, not 568.8 credits.
// No separate enterprise credit discount has been supplied for these models.
const WAN_CREDIT_HUNDREDTHS_PER_SECOND: [u64; 3] = [998, 1996, 3991];
const PRIME_CREDIT_HUNDREDTHS_PER_SECOND: [u64; 3] = [1886, 3883, 7766];
/// Shared by all six Wan operations. Defaults to 16:9.
/// `adaptive` is REST-only for reference-to-video and is not a Studio option.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum KinoviWan3p0AspectRatio {
#[default]
Landscape16x9,
Portrait9x16,
Landscape4x3,
Portrait3x4,
Square1x1,
}
/// Shared by regular and Prime Wan. Defaults to 720p, pinned on the wire.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum KinoviWan3p0OutputResolution {
FourEightyP,
#[default]
SevenTwentyP,
TenEightyP,
}
#[derive(Clone, Copy, Debug)]
pub(super) enum Wan3p0Model {
TextToVideo,
ImageToVideo,
RefToVideo,
PrimeTextToVideo,
PrimeImageToVideo,
PrimeRefToVideo,
}
/// Consumer tRPC shape, verified against the 2026-09-18 captures. Images
/// use `uploadedUrls`; videos use BOTH `videoUrl` and `videoUrls`.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct Wan3p0ApiParams {
model: &'static str,
content_mode: &'static str,
face_blur_mode: &'static str,
prompt: String,
#[serde(skip_serializing_if = "Option::is_none")]
mode: Option<&'static str>,
aspect_ratio: &'static str,
duration: String,
output_resolution: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
seed: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
audio: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(super) uploaded_urls: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
video_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(super) video_urls: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(super) audio_urls: Option<Vec<String>>,
}
impl Wan3p0Model {
pub(super) fn calculate_costs(
self,
resolution: Option<KinoviWan3p0OutputResolution>,
duration_seconds: u8,
tier: KinoviPricingTier,
) -> KinoviFractionalGenerationCost {
let rates = match self {
Self::TextToVideo | Self::ImageToVideo | Self::RefToVideo => WAN_CREDIT_HUNDREDTHS_PER_SECOND,
Self::PrimeTextToVideo | Self::PrimeImageToVideo | Self::PrimeRefToVideo => PRIME_CREDIT_HUNDREDTHS_PER_SECOND,
};
let index = match resolution.unwrap_or_default() {
KinoviWan3p0OutputResolution::FourEightyP => 0,
KinoviWan3p0OutputResolution::SevenTwentyP => 1,
KinoviWan3p0OutputResolution::TenEightyP => 2,
};
let credit_hundredths = rates[index] * u64::from(duration_seconds);
tier.cost_from_credits(credit_hundredths as f64 / 100.0)
}
}
impl Wan3p0ApiParams {
pub(super) fn new(
model: Wan3p0Model,
prompt: String,
aspect_ratio: Option<KinoviWan3p0AspectRatio>,
resolution: Option<KinoviWan3p0OutputResolution>,
duration_seconds: u8,
seed: Option<u32>,
audio: Option<bool>,
) -> Result<Self, KinoviWebError> {
if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&duration_seconds) {
return Err(invalid_field("duration_seconds", duration_seconds, "must be between 2 and 30"));
}
if let Some(seed) = seed {
if seed > MAX_SEED {
return Err(invalid_field("maybe_seed", seed, "must be between 0 and 2147483647"));
}
}
if prompt.chars().count() > 20_000 {
return Err(invalid_field("prompt", prompt.chars().count(), "must not exceed 20000 characters"));
}
let (model, mode) = match model {
Wan3p0Model::TextToVideo => ("wan3.0-text-to-video", None),
Wan3p0Model::ImageToVideo => ("wan3.0-image-to-video", Some("keyframe")),
Wan3p0Model::RefToVideo => ("wan3.0-ref-to-video", Some("reference")),
Wan3p0Model::PrimeTextToVideo => ("wan3.0-prime-text-to-video", None),
Wan3p0Model::PrimeImageToVideo => ("wan3.0-prime-image-to-video", Some("keyframe")),
Wan3p0Model::PrimeRefToVideo => ("wan3.0-prime-ref-to-video", Some("reference")),
};
Ok(Self {
model,
content_mode: "normal",
face_blur_mode: "off",
prompt,
mode,
aspect_ratio: match aspect_ratio.unwrap_or_default() {
KinoviWan3p0AspectRatio::Landscape16x9 => "16:9",
KinoviWan3p0AspectRatio::Portrait9x16 => "9:16",
KinoviWan3p0AspectRatio::Landscape4x3 => "4:3",
KinoviWan3p0AspectRatio::Portrait3x4 => "3:4",
KinoviWan3p0AspectRatio::Square1x1 => "1:1",
},
duration: format!("{duration_seconds}s"),
output_resolution: match resolution.unwrap_or_default() {
KinoviWan3p0OutputResolution::FourEightyP => "480p",
KinoviWan3p0OutputResolution::SevenTwentyP => "720p",
KinoviWan3p0OutputResolution::TenEightyP => "1080p",
},
seed,
audio,
uploaded_urls: None,
video_url: None,
video_urls: None,
audio_urls: None,
})
}
pub(super) fn validate_media(
&mut self,
duration_seconds: u8,
maybe_total_reference_video_duration_millis: Option<u32>,
) -> Result<(), KinoviWebError> {
self.uploaded_urls = self.uploaded_urls.take().filter(|urls| !urls.is_empty());
self.video_urls = self.video_urls.take().filter(|urls| !urls.is_empty());
self.audio_urls = self.audio_urls.take().filter(|urls| !urls.is_empty());
let image_count = self.uploaded_urls.as_ref().map_or(0, Vec::len);
let video_count = self.video_urls.as_ref().map_or(0, Vec::len);
let audio_count = self.audio_urls.as_ref().map_or(0, Vec::len);
let max_images = if self.mode == Some("keyframe") { 2 } else { 10 };
for (field, count, maximum) in [
("maybe_reference_image_urls", image_count, max_images),
("maybe_reference_video_urls", video_count, 5),
("maybe_reference_audio_urls", audio_count, 5),
] {
if count > maximum {
return Err(invalid_field(field, count, format!("at most {maximum} items are supported")));
}
}
if self.mode == Some("keyframe") && image_count == 0 {
return Err(invalid_field("maybe_start_frame_url", "", "image-to-video requires a start frame"));
}
if self.prompt.trim().is_empty() && image_count + video_count + audio_count == 0 {
return Err(invalid_field("prompt", "", "a prompt is required when no media is supplied"));
}
for urls in [&self.uploaded_urls, &self.video_urls, &self.audio_urls].into_iter().flatten() {
if urls.iter().any(|url| url.trim().is_empty()) {
return Err(invalid_field("media_url", "", "media URLs must not be empty"));
}
}
if video_count > 0 {
// Without measured metadata, only enforce the known lower bound:
// each reference video must be at least 1s. Kinovi validates actual
// media durations; the client does not download/probe URLs here.
let minimum_millis = video_count as u32 * 1_000;
let input_millis = maybe_total_reference_video_duration_millis.unwrap_or(minimum_millis);
if !(minimum_millis..=MAX_REFERENCE_VIDEO_DURATION_MILLIS).contains(&input_millis) {
return Err(invalid_field(
"maybe_total_reference_video_duration_millis",
input_millis,
"reference videos must each be 1-15s and total at most 15s",
));
}
if input_millis + u32::from(duration_seconds) * 1_000 > 30_000 {
return Err(invalid_field(
"duration_seconds",
duration_seconds,
"reference-video duration plus output duration must not exceed 30s",
));
}
self.video_url = self.video_urls.as_ref().and_then(|urls| urls.first()).cloned();
}
Ok(())
}
}
pub(super) fn invalid_field(field: &'static str, value: impl ToString, reason: impl Into<String>) -> KinoviWebError {
KinoviWebClientError::InvalidRequestField {
field,
raw_value: value.to_string(),
reason: reason.into(),
}
.into()
}
@@ -0,0 +1,10 @@
{
"model": "wan3.0-ref-to-video",
"contentMode": "normal",
"faceBlurMode": "off",
"prompt": "A corgi eats a watermelon",
"mode": "reference",
"aspectRatio": "16:9",
"duration": "5s",
"outputResolution": "720p"
}
@@ -0,0 +1,9 @@
{
"model": "wan3.0-text-to-video",
"contentMode": "normal",
"faceBlurMode": "off",
"prompt": "A shiba eats a pie",
"aspectRatio": "16:9",
"duration": "5s",
"outputResolution": "720p"
}
@@ -0,0 +1,13 @@
{
"model": "wan3.0-prime-image-to-video",
"contentMode": "normal",
"faceBlurMode": "off",
"prompt": "A lighthouse over the coast",
"mode": "keyframe",
"aspectRatio": "16:9",
"duration": "5s",
"outputResolution": "720p",
"uploadedUrls": [
"https://static.seedance2-pro.com/materials/20260918/1789739927378-438b78db.png"
]
}
@@ -0,0 +1,21 @@
{
"model": "wan3.0-ref-to-video",
"contentMode": "normal",
"faceBlurMode": "off",
"prompt": "The Image 2 is on the beach Video 1 . There's a lighthouse Image 1 . The shiba says Audio 1",
"mode": "reference",
"aspectRatio": "4:3",
"duration": "5s",
"outputResolution": "1080p",
"videoUrl": "https://static.seedance2-pro.com/materials/20260918/1789740067508-9b952e67.mp4",
"videoUrls": [
"https://static.seedance2-pro.com/materials/20260918/1789740067508-9b952e67.mp4"
],
"audioUrls": [
"https://static.seedance2-pro.com/materials/20260918/1789740228370-3f5d9623.wav"
],
"uploadedUrls": [
"https://static.seedance2-pro.com/materials/20260918/1789740059548-4149c254.png",
"https://static.seedance2-pro.com/materials/20260918/1789740077221-9702e67d.png"
]
}
@@ -0,0 +1,14 @@
{
"model": "wan3.0-prime-ref-to-video",
"contentMode": "normal",
"faceBlurMode": "off",
"prompt": "There's a shop full of lanterns. the camera orbits.",
"mode": "reference",
"aspectRatio": "16:9",
"duration": "10s",
"outputResolution": "480p",
"seed": 2028741236,
"uploadedUrls": [
"https://static.seedance2-pro.com/materials/20260918/1789740399001-2bb62f21.jpg"
]
}
@@ -0,0 +1,9 @@
{
"model": "wan3.0-prime-text-to-video",
"contentMode": "normal",
"faceBlurMode": "off",
"prompt": "A bowl of soup is knocked over by a knight",
"aspectRatio": "16:9",
"duration": "5s",
"outputResolution": "720p"
}
@@ -0,0 +1,15 @@
{
"model": "wan3.0-prime-image-to-video",
"contentMode": "normal",
"faceBlurMode": "off",
"prompt": "The lanterns blow in the breeze above the water",
"mode": "keyframe",
"aspectRatio": "16:9",
"duration": "4s",
"outputResolution": "1080p",
"seed": 1878107760,
"uploadedUrls": [
"https://static.seedance2-pro.com/materials/20260918/1789740716787-bf6a26f8.jpg",
"https://static.seedance2-pro.com/materials/20260918/1789740722438-0101d158.jpg"
]
}
@@ -0,0 +1,523 @@
use std::time::Duration;
use serde_json::{json, Value};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::task::JoinHandle;
use tokio::time::timeout;
use super::generate_wan_3p0_image_to_video::*;
use super::generate_wan_3p0_prime_image_to_video::*;
use super::generate_wan_3p0_prime_ref_to_video::*;
use super::generate_wan_3p0_prime_text_to_video::*;
use super::generate_wan_3p0_ref_to_video::*;
use super::generate_wan_3p0_text_to_video::*;
use super::wan_3p0::{KinoviWan3p0AspectRatio, KinoviWan3p0OutputResolution, Wan3p0ApiParams};
use crate::creds::kinovi_web_session::KinoviWebSession;
use crate::error::kinovi_web_client_error::KinoviWebClientError;
use crate::error::kinovi_web_error::KinoviWebError;
use crate::pricing::kinovi_cost_calculator_trait::KinoviCostCalculatorTrait;
use crate::pricing::kinovi_pricing_tier::KinoviPricingTier;
use crate::requests::kinovi_host::KinoviHost;
const RESOLUTIONS: [KinoviWan3p0OutputResolution; 3] = [
KinoviWan3p0OutputResolution::FourEightyP,
KinoviWan3p0OutputResolution::SevenTwentyP,
KinoviWan3p0OutputResolution::TenEightyP,
];
// Literal price snapshots from the supplied observations and Kinovi's model
// pages, checked 2026-09-18. Keep independent of the calculator's rate tables.
// Columns are 480p, 720p, 1080p; all three modalities have the same rates.
// WAN_CREDITS and WAN_CENTS intentionally assert promotional prices until a
// reviewed update after 2026-09-23. See costs_wan_3p0.md at the crate root for
// advertised replacement rates and all expectations that need to change.
const WAN_CREDITS: [(u8, [f64; 3]); 9] = [
(2, [19.96, 39.92, 79.82]),
(5, [49.9, 99.8, 199.55]),
(8, [79.84, 159.68, 319.28]),
(10, [99.8, 199.6, 399.1]),
(15, [149.7, 299.4, 598.65]),
(20, [199.6, 399.2, 798.2]),
(22, [219.56, 439.12, 878.02]),
(25, [249.5, 499.0, 997.75]),
(30, [299.4, 598.8, 1197.3]),
];
const PRIME_CREDITS: [(u8, [f64; 3]); 9] = [
(2, [37.72, 77.66, 155.32]),
(5, [94.3, 194.15, 388.3]),
(8, [150.88, 310.64, 621.28]),
(10, [188.6, 388.3, 776.6]),
(15, [282.9, 582.45, 1164.9]),
(20, [377.2, 776.6, 1553.2]),
(22, [414.92, 854.26, 1708.52]),
(25, [471.5, 970.75, 1941.5]),
// The supplied Prime reference 480p/30s figure said 568.8; docs say 565.8.
(30, [565.8, 1164.9, 2329.8]),
];
// Thirty-second USD cents, (rounded down, rounded up), consumer then enterprise.
const WAN_CENTS: [[(u64, u64); 3]; 2] = [[(155, 156), (310, 311), (620, 621)], [(123, 124), (246, 247), (492, 493)]];
const PRIME_CENTS: [[(u64, u64); 3]; 2] =
[[(293, 294), (603, 604), (1207, 1208)], [(232, 233), (479, 480), (958, 959)]];
// Only sanitized apiParams are included; the original captures contain cookies.
const CAPTURES: [&str; 7] = [
include_str!("wan_3p0_test_data/01_wan_ref_to_video_without_ref.json"),
include_str!("wan_3p0_test_data/02_wan_text_to_video.json"),
include_str!("wan_3p0_test_data/03_wan_image_to_video.json"),
include_str!("wan_3p0_test_data/04_wan_ref_to_video_full.json"),
include_str!("wan_3p0_test_data/05_wan_prime_ref_to_video.json"),
include_str!("wan_3p0_test_data/06_wan_prime_text_to_video.json"),
include_str!("wan_3p0_test_data/07_wan_prime_image_to_video.json"),
];
macro_rules! operation_tests {
($module:ident, $request:ident, $args:ident, $generate:ident, $model:literal,
$credits:ident, $cents:ident, {$($field:ident: $value:expr),* $(,)?}) => {
mod $module {
use super::*;
#[test]
fn literal_credit_prices_at_every_resolution_and_observed_duration() {
for tier in [KinoviPricingTier::Consumer, KinoviPricingTier::Enterprise] {
for (duration, expected) in $credits {
for (resolution, credits) in RESOLUTIONS.into_iter().zip(expected) {
// Cost estimates work before media has been uploaded.
let request = $request {
duration_seconds: duration,
maybe_output_resolution: Some(resolution),
..Default::default()
};
assert_eq!(request.calculate_costs(tier).kinovi_credits, credits,
"{tier:?}, {resolution:?}, {duration}s");
}
}
}
}
#[test]
fn literal_usd_cent_prices_for_both_tiers() {
for (tier, expected) in [KinoviPricingTier::Consumer, KinoviPricingTier::Enterprise]
.into_iter().zip($cents)
{
for (resolution, (down, up)) in RESOLUTIONS.into_iter().zip(expected) {
let request = $request {
duration_seconds: 30,
maybe_output_resolution: Some(resolution),
..Default::default()
};
let cost = request.calculate_costs(tier);
assert_eq!((cost.usd_cents_rounded_down, cost.usd_cents_rounded_up), (down, up));
}
}
}
#[test]
fn defaults_match_the_priced_resolution() {
let request = valid_request();
assert_eq!(request.calculate_consumer_costs().kinovi_credits, $credits[1].1[1]);
let params = serde_json::to_value(request.build_api_params().unwrap()).unwrap();
assert_eq!(params["model"], $model);
assert_eq!(params["duration"], "5s");
assert_eq!(params["outputResolution"], "720p");
assert_eq!(params["aspectRatio"], "16:9");
assert!(params.get("seed").is_none());
assert!(params.get("audio").is_none());
}
#[test]
fn all_whole_second_durations_and_aspect_ratios_serialize() {
for duration in 2..=30 {
let mut request = valid_request();
request.duration_seconds = duration;
let params = serde_json::to_value(request.build_api_params().unwrap()).unwrap();
assert_eq!(params["duration"], format!("{duration}s"));
}
for (maybe_aspect_ratio, expected) in [
(KinoviWan3p0AspectRatio::Landscape16x9, "16:9"),
(KinoviWan3p0AspectRatio::Portrait9x16, "9:16"),
(KinoviWan3p0AspectRatio::Landscape4x3, "4:3"),
(KinoviWan3p0AspectRatio::Portrait3x4, "3:4"),
(KinoviWan3p0AspectRatio::Square1x1, "1:1"),
] {
let mut request = valid_request();
request.maybe_aspect_ratio = Some(maybe_aspect_ratio);
let params = serde_json::to_value(request.build_api_params().unwrap()).unwrap();
assert_eq!(params["aspectRatio"], expected);
}
for (resolution, expected) in RESOLUTIONS.into_iter().zip(["480p", "720p", "1080p"]) {
let mut request = valid_request();
request.maybe_output_resolution = Some(resolution);
let params = serde_json::to_value(request.build_api_params().unwrap()).unwrap();
assert_eq!(params["outputResolution"], expected);
}
}
#[test]
fn seed_and_audio_flags_preserve_values_without_changing_price() {
for (seed, audio) in [(0, false), (2_147_483_647, true)] {
let mut request = valid_request();
request.maybe_seed = Some(seed);
request.maybe_generate_audio = Some(audio);
assert_eq!(request.calculate_consumer_costs().kinovi_credits, $credits[1].1[1]);
let params = serde_json::to_value(request.build_api_params().unwrap()).unwrap();
assert_eq!(params["seed"], seed);
assert_eq!(params["audio"], audio);
}
}
#[test]
fn invalid_duration_seed_and_prompt_are_rejected() {
for duration in [0, 1, 31, 255] {
let mut request = valid_request();
request.duration_seconds = duration;
assert_invalid(request.build_api_params(), "duration_seconds");
}
for seed in [2_147_483_648, u32::MAX] {
let mut request = valid_request();
request.maybe_seed = Some(seed);
assert_invalid(request.build_api_params(), "maybe_seed");
}
let mut request = valid_request();
request.prompt = "猫".repeat(20_000);
assert!(request.clone().build_api_params().is_ok());
request.prompt.push('猫');
assert_invalid(request.build_api_params(), "prompt");
}
#[tokio::test]
async fn public_generate_sends_workflow_request_and_returns_task_ids() {
let (host, server) = stub_server().await;
let session = KinoviWebSession::from_cookies_string("test-session=local".to_owned());
let request = valid_request();
let params = serde_json::to_value(request.clone().build_api_params().unwrap()).unwrap();
let result = timeout(Duration::from_secs(10), $generate($args {
request,
session: &session,
maybe_host_override: Some(host),
})).await.unwrap().unwrap();
assert_eq!(result.task_id, "wan-task");
assert_eq!(result.order_id, "wan-order");
assert_eq!(result.task_ids, Some(vec!["wan-task".to_owned()]));
assert_eq!(result.order_ids, Some(vec!["wan-order".to_owned()]));
let (headers, body) = timeout(Duration::from_secs(10), server).await.unwrap().unwrap();
assert!(headers.starts_with("POST /api/trpc/workflow.runTask?batch=1 HTTP/1.1\r\n"));
assert_eq!(body, json!({"0": {"json": {
"businessType": "wan3-video-generation", "apiParams": params,
}}}));
}
fn valid_request() -> $request {
$request {
prompt: "A lighthouse on the coast".to_owned(),
$($field: $value,)*
..Default::default()
}
}
}
};
}
operation_tests!(
text,
GenerateWan3p0TextToVideoRequest,
GenerateWan3p0TextToVideoArgs,
generate_wan_3p0_text_to_video,
"wan3.0-text-to-video",
WAN_CREDITS,
WAN_CENTS,
{}
);
operation_tests!(image, GenerateWan3p0ImageToVideoRequest, GenerateWan3p0ImageToVideoArgs,
generate_wan_3p0_image_to_video, "wan3.0-image-to-video", WAN_CREDITS, WAN_CENTS,
{ maybe_start_frame_url: Some("https://example.com/start.png".to_owned()) });
operation_tests!(
reference,
GenerateWan3p0RefToVideoRequest,
GenerateWan3p0RefToVideoArgs,
generate_wan_3p0_ref_to_video,
"wan3.0-ref-to-video",
WAN_CREDITS,
WAN_CENTS,
{}
);
operation_tests!(
prime_text,
GenerateWan3p0PrimeTextToVideoRequest,
GenerateWan3p0PrimeTextToVideoArgs,
generate_wan_3p0_prime_text_to_video,
"wan3.0-prime-text-to-video",
PRIME_CREDITS,
PRIME_CENTS,
{}
);
operation_tests!(prime_image, GenerateWan3p0PrimeImageToVideoRequest, GenerateWan3p0PrimeImageToVideoArgs,
generate_wan_3p0_prime_image_to_video, "wan3.0-prime-image-to-video", PRIME_CREDITS, PRIME_CENTS,
{ maybe_start_frame_url: Some("https://example.com/start.png".to_owned()) });
operation_tests!(
prime_reference,
GenerateWan3p0PrimeRefToVideoRequest,
GenerateWan3p0PrimeRefToVideoArgs,
generate_wan_3p0_prime_ref_to_video,
"wan3.0-prime-ref-to-video",
PRIME_CREDITS,
PRIME_CENTS,
{}
);
#[test]
fn payloads_match_all_seven_consumer_captures() {
for fixture in CAPTURES {
let expected: Value = serde_json::from_str(fixture).unwrap();
assert_eq!(params_from_capture(&expected), expected);
}
// Capture 03's filename says Wan image, but its actual model is Prime.
// Exercise regular image too, using the same documented keyframe shape.
let mut regular_image: Value = serde_json::from_str(CAPTURES[2]).unwrap();
regular_image["model"] = json!("wan3.0-image-to-video");
assert_eq!(params_from_capture(&regular_image), regular_image);
}
macro_rules! reference_media_tests {
($module:ident, $request:ident, $credits:literal) => {
mod $module {
use super::*;
#[test]
fn references_are_optional_and_empty_lists_are_omitted() {
let request = $request {
prompt: "A ship at sea".to_owned(),
duration_seconds: 30,
maybe_reference_image_urls: Some(vec![]),
maybe_reference_video_urls: Some(vec![]),
maybe_reference_audio_urls: Some(vec![]),
..Default::default()
};
let params = serde_json::to_value(request.build_api_params().unwrap()).unwrap();
for field in ["uploadedUrls", "videoUrl", "videoUrls", "audioUrls"] {
assert!(params.get(field).is_none(), "{field}");
}
assert_invalid($request::default().build_api_params(), "prompt");
}
#[test]
fn reference_duration_and_output_share_thirty_seconds() {
let request = $request {
duration_seconds: 20,
maybe_reference_video_urls: Some(urls(1)),
maybe_total_reference_video_duration_millis: Some(10_000),
..Default::default()
};
assert!(request.clone().build_api_params().is_ok());
for millis in [0, 999, 10_001, 15_001, u32::MAX] {
let mut invalid = request.clone();
invalid.maybe_total_reference_video_duration_millis = Some(millis);
assert!(invalid.build_api_params().is_err(), "{millis}ms");
}
let mut request = request;
request.duration_seconds = 15;
request.maybe_total_reference_video_duration_millis = Some(15_000);
assert!(request.clone().build_api_params().is_ok());
request.duration_seconds = 16;
assert_invalid(request.clone().build_api_params(), "duration_seconds");
request.maybe_total_reference_video_duration_millis = None;
request.duration_seconds = 29;
assert!(request.clone().build_api_params().is_ok());
request.duration_seconds = 30;
assert_invalid(request.build_api_params(), "duration_seconds");
}
#[test]
fn all_reference_types_have_independent_limits_and_no_surcharge() {
let request = $request {
duration_seconds: 10,
maybe_reference_image_urls: Some(urls(10)),
maybe_reference_video_urls: Some(urls(5)),
maybe_reference_audio_urls: Some(urls(5)),
maybe_total_reference_video_duration_millis: Some(15_000),
..Default::default()
};
assert_eq!(request.calculate_consumer_costs().kinovi_credits, $credits);
let params = serde_json::to_value(request.clone().build_api_params().unwrap()).unwrap();
assert_eq!(params["uploadedUrls"], json!(urls(10)));
assert_eq!(params["videoUrls"], json!(urls(5)));
assert_eq!(params["videoUrl"], urls(1)[0]);
assert_eq!(params["audioUrls"], json!(urls(5)));
assert!(params.get("maybeTotalReferenceVideoDurationMillis").is_none());
assert!(params.get("imageUrls").is_none());
let mut invalid = request.clone();
invalid.maybe_reference_image_urls = Some(urls(11));
assert_invalid(invalid.build_api_params(), "maybe_reference_image_urls");
let mut invalid = request.clone();
invalid.maybe_reference_video_urls = Some(urls(6));
assert_invalid(invalid.build_api_params(), "maybe_reference_video_urls");
let mut invalid = request.clone();
invalid.maybe_reference_audio_urls = Some(urls(6));
assert_invalid(invalid.build_api_params(), "maybe_reference_audio_urls");
let mut invalid = request.clone();
invalid.maybe_total_reference_video_duration_millis = Some(4_999);
assert_invalid(invalid.build_api_params(), "maybe_total_reference_video_duration_millis");
let mut invalid = request;
invalid.maybe_reference_audio_urls = Some(vec![" ".to_owned()]);
assert_invalid(invalid.build_api_params(), "media_url");
}
}
};
}
reference_media_tests!(reference_media, GenerateWan3p0RefToVideoRequest, 199.6);
reference_media_tests!(prime_reference_media, GenerateWan3p0PrimeRefToVideoRequest, 388.3);
#[test]
fn keyframes_require_a_start_image_and_preserve_start_end_order() {
macro_rules! check {
($request:ident, $credits:literal) => {
let mut request = $request {
prompt: "Waves".to_owned(),
..Default::default()
};
assert_invalid(request.clone().build_api_params(), "maybe_start_frame_url");
request.maybe_end_frame_url = Some("https://example.com/end.png".to_owned());
assert_invalid(request.clone().build_api_params(), "maybe_end_frame_url");
request.maybe_start_frame_url = Some("https://example.com/start.png".to_owned());
request.prompt.clear();
assert_eq!(request.calculate_consumer_costs().kinovi_credits, $credits);
let params = serde_json::to_value(request.build_api_params().unwrap()).unwrap();
assert_eq!(params["uploadedUrls"], json!(["https://example.com/start.png", "https://example.com/end.png",]));
assert!(params.get("imageUrls").is_none());
};
}
check!(GenerateWan3p0ImageToVideoRequest, 99.8);
check!(GenerateWan3p0PrimeImageToVideoRequest, 194.15);
}
#[test]
fn text_operations_require_a_prompt() {
assert_invalid(GenerateWan3p0TextToVideoRequest::default().build_api_params(), "prompt");
assert_invalid(GenerateWan3p0PrimeTextToVideoRequest::default().build_api_params(), "prompt");
}
fn params_from_capture(value: &Value) -> Value {
macro_rules! build {
($request:ident, {$($field:ident: $extra:expr),* $(,)?}) => {
$request {
prompt: value["prompt"].as_str().unwrap().to_owned(),
maybe_aspect_ratio: Some(match value["aspectRatio"].as_str().unwrap() {
"16:9" => KinoviWan3p0AspectRatio::Landscape16x9,
"4:3" => KinoviWan3p0AspectRatio::Landscape4x3,
other => panic!("unexpected fixture aspect ratio: {other}"),
}),
maybe_output_resolution: Some(match value["outputResolution"].as_str().unwrap() {
"480p" => KinoviWan3p0OutputResolution::FourEightyP,
"720p" => KinoviWan3p0OutputResolution::SevenTwentyP,
"1080p" => KinoviWan3p0OutputResolution::TenEightyP,
other => panic!("unexpected fixture resolution: {other}"),
}),
duration_seconds: value["duration"].as_str().unwrap().trim_end_matches('s').parse().unwrap(),
maybe_seed: value["seed"].as_u64().map(|seed| seed as u32),
$($field: $extra,)*
..Default::default()
}.build_api_params().unwrap()
};
}
macro_rules! image {
($request:ident) => {
build!($request, {
maybe_start_frame_url: value["uploadedUrls"][0].as_str().map(str::to_owned),
maybe_end_frame_url: value["uploadedUrls"][1].as_str().map(str::to_owned),
})
};
}
macro_rules! reference {
($request:ident) => {
build!($request, {
maybe_reference_image_urls: capture_urls(value, "uploadedUrls"),
maybe_reference_video_urls: capture_urls(value, "videoUrls"),
maybe_reference_audio_urls: capture_urls(value, "audioUrls"),
})
};
}
let params = match value["model"].as_str().unwrap() {
"wan3.0-text-to-video" => build!(GenerateWan3p0TextToVideoRequest, {}),
"wan3.0-image-to-video" => image!(GenerateWan3p0ImageToVideoRequest),
"wan3.0-ref-to-video" => reference!(GenerateWan3p0RefToVideoRequest),
"wan3.0-prime-text-to-video" => build!(GenerateWan3p0PrimeTextToVideoRequest, {}),
"wan3.0-prime-image-to-video" => image!(GenerateWan3p0PrimeImageToVideoRequest),
"wan3.0-prime-ref-to-video" => reference!(GenerateWan3p0PrimeRefToVideoRequest),
other => panic!("unexpected fixture model: {other}"),
};
serde_json::to_value(params).unwrap()
}
async fn stub_server() -> (KinoviHost, JoinHandle<(String, Value)>) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = format!("http://{}", listener.local_addr().unwrap());
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
let mut bytes = Vec::new();
let (headers, body_start, body_len) = loop {
let mut chunk = [0; 4096];
let length = stream.read(&mut chunk).await.unwrap();
assert_ne!(length, 0, "request ended before headers");
bytes.extend_from_slice(&chunk[..length]);
if let Some(end) = bytes.windows(4).position(|window| window == b"\r\n\r\n") {
let headers = String::from_utf8(bytes[..end + 4].to_vec()).unwrap();
let body_len: usize = headers
.lines()
.find_map(|line| {
let (name, value) = line.split_once(':')?;
name.eq_ignore_ascii_case("content-length").then(|| value.trim().parse().unwrap())
})
.unwrap();
break (headers, end + 4, body_len);
}
};
while bytes.len() < body_start + body_len {
let mut chunk = [0; 4096];
let length = stream.read(&mut chunk).await.unwrap();
assert_ne!(length, 0, "request ended before body");
bytes.extend_from_slice(&chunk[..length]);
}
let body = serde_json::from_slice(&bytes[body_start..body_start + body_len]).unwrap();
let response = json!([{"result": {"data": {"json": {
"taskId": "wan-task", "orderId": "wan-order",
"taskIds": ["wan-task"], "orderIds": ["wan-order"], "violationWarning": false,
}}}}])
.to_string();
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
response.len(),
response,
);
stream.write_all(response.as_bytes()).await.unwrap();
(headers, body)
});
(
KinoviHost::CustomHost {
api_host: address.clone(),
cdn_host: address,
},
server,
)
}
fn capture_urls(value: &Value, field: &str) -> Option<Vec<String>> {
value[field]
.as_array()
.map(|urls| urls.iter().map(|url| url.as_str().unwrap().to_owned()).collect())
}
fn urls(count: usize) -> Vec<String> {
(0..count).map(|index| format!("https://example.com/reference-{index}")).collect()
}
fn assert_invalid(result: Result<Wan3p0ApiParams, KinoviWebError>, field: &str) {
match result.unwrap_err() {
KinoviWebError::Client(KinoviWebClientError::InvalidRequestField {
field: actual,
..
}) => {
assert_eq!(actual, field);
},
other => panic!("expected invalid {field}, got {other:?}"),
}
}
@@ -816,6 +816,30 @@ mod tests {
assert!(json.contains(r#""batchCount":8"#), "{json}");
}
#[test]
fn batch_count_serializes_for_all_video_models() {
for model in [
KinoviModelTypeRaw::Seedance2Pro,
KinoviModelTypeRaw::Seedance2Fast,
KinoviModelTypeRaw::Seedance2Mini,
KinoviModelTypeRaw::Seedance2p5,
KinoviModelTypeRaw::Seedance2p5Preview,
KinoviModelTypeRaw::HappyHorse1p0,
] {
let batches = match model {
KinoviModelTypeRaw::HappyHorse1p0 => [(KinoviBatchCountRaw::One, None), (KinoviBatchCountRaw::Three, Some(3))],
_ => [(KinoviBatchCountRaw::One, None), (KinoviBatchCountRaw::Eight, Some(8))],
};
for (batch, expected) in batches {
let mut request = mini_request(KinoviAspectRatioRaw::Landscape16x9, None, batch);
request.model_type = model;
let body = serde_json::to_value(build_batch_request(request)).unwrap();
let params = &body["0"]["json"]["apiParams"];
assert_eq!(params.get("batchCount"), expected.map(serde_json::Value::from).as_ref(), "{model:?}");
}
}
}
#[test]
fn non_mini_still_uses_resolution_field() {
let mut req = mini_request(KinoviAspectRatioRaw::Landscape16x9, None, KinoviBatchCountRaw::One);
@@ -0,0 +1,34 @@
use crate::pricing::cost::kinovi_fractional_generation_cost::KinoviFractionalGenerationCost;
use crate::pricing::kinovi_pricing_tier::KinoviPricingTier;
const FLOAT_TOLERANCE: f64 = 1e-8;
/// Check scaling of credits and fractional cents, then independently compute
/// whole cents from the complete batch using integer arithmetic. Multiplying
/// a single video's already-rounded cents can overcharge a batch.
pub(crate) fn assert_batch_cost(
single: KinoviFractionalGenerationCost,
batched: KinoviFractionalGenerationCost,
count: u64,
tier: KinoviPricingTier,
context: &str,
) {
let expected_credit_hundredths = (single.kinovi_credits * 100.0).round() as u64 * count;
let expected_credits = expected_credit_hundredths as f64 / 100.0;
let expected_fractional_cents = single.usd_cents_fractional * count as f64;
assert_eq!(batched.kinovi_credits, expected_credits, "{context}: credits");
assert!(
(batched.usd_cents_fractional - expected_fractional_cents).abs() < FLOAT_TOLERANCE,
"{context}: fractional cents: {} != {expected_fractional_cents}",
batched.usd_cents_fractional,
);
// Credit purchase rates expressed in hundredths of a credit per dollar.
let credits_per_dollar_hundredths = match tier {
KinoviPricingTier::Consumer => 19_298,
KinoviPricingTier::Enterprise => 24_316,
};
let numerator = expected_credit_hundredths * 100;
assert_eq!(batched.usd_cents_rounded_up, numerator.div_ceil(credits_per_dollar_hundredths), "{context}: rounded up");
assert_eq!(batched.usd_cents_rounded_down, numerator / credits_per_dollar_hundredths, "{context}: rounded down");
}
@@ -1,3 +1,6 @@
#[cfg(test)]
pub (crate) mod assert_batch_cost;
#[cfg(test)]
pub (crate) mod get_test_cookies;
@@ -27,7 +27,7 @@ pub fn happy_horse_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonResolution::TenEightyP,
]),
resolution_default: Some(CommonResolution::SevenTwentyP),
batch_size_options: Some(vec![1, 2, 4]),
batch_size_options: Some(vec![1, 2, 3, 4]),
batch_size_default: Some(1),
duration_seconds_min: Some(3),
duration_seconds_max: Some(15),
@@ -51,7 +51,7 @@ pub fn seedance_2p5_video_models() -> Vec<OmniGenVideoModelDetails> {
bitrate_default: Some(CommonBitrate::Normal),
output_format_options: Some(vec![CommonVideoOutputFormat::Mp4, CommonVideoOutputFormat::Mov]),
output_format_default: Some(CommonVideoOutputFormat::Mp4),
batch_size_options: Some(vec![1]),
batch_size_options: Some(vec![1, 2, 3, 4]),
batch_size_default: Some(1),
duration_seconds_min: Some(4),
duration_seconds_max: Some(30),
@@ -103,7 +103,7 @@ pub fn seedance_2p5_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonVideoOutputFormat::Mov
]),
output_format_default: Some(CommonVideoOutputFormat::Mp4),
batch_size_options: Some(vec![1]),
batch_size_options: Some(vec![1, 2, 3, 4]),
batch_size_default: Some(1),
duration_seconds_min: Some(4),
duration_seconds_max: Some(30),
@@ -156,3 +156,31 @@ fn build_omni_gen_video_model_providers() -> Vec<OmniGenVideoModelProviderDetail
providers
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kinovi_batch_options_are_one_through_four() {
let models = build_omni_gen_video_models();
for model in [
CommonVideoModel::Seedance2p5,
CommonVideoModel::Seedance2p0,
CommonVideoModel::Seedance2p0Fast,
CommonVideoModel::Seedance2p0Mini,
CommonVideoModel::Seedance2p5Ultra,
CommonVideoModel::Seedance2p0BytePlus,
CommonVideoModel::Seedance2p0BytePlusFast,
CommonVideoModel::Seedance2p0BytePlusMini,
CommonVideoModel::Seedance2p0BytePlusUltra,
CommonVideoModel::Seedance2p0BytePlusUltraFast,
CommonVideoModel::Seedance2p0BytePlusUltraMini,
CommonVideoModel::HappyHorse1p0,
] {
let details = models.iter().find(|details| details.model == model).unwrap();
assert_eq!(details.batch_size_options.as_deref(), Some([1, 2, 3, 4].as_slice()), "{model:?}");
assert_eq!(details.batch_size_default, Some(1), "{model:?}");
}
}
}
@@ -137,3 +137,58 @@ fn to_omni_gen_request(request: OmniApiVideoGenerateRequest) -> OmniGenVideoCost
estimate_only: None,
}
}
#[cfg(test)]
mod tests {
use artcraft_router::api::router_provider::RouterProvider;
use enums::common::generation::common_video_model::CommonVideoModel;
use serde_json::json;
use crate::http_server::endpoints::omni_gen::generate::video::helpers::hydrate_router_request::hydrate_to_router_request;
use crate::http_server::endpoints::omni_gen::shared_utils::video::validate_video_request::validate_video_request as validate_omni_gen;
use super::*;
#[test]
fn kinovi_batches_reach_shared_generation_with_the_full_retail_price() {
// Both handlers use this hydration and ArtCraft pricing path. The API
// handler additionally converts its request into the omni_gen shape.
for (model, expected_prices) in [
(CommonVideoModel::Seedance2p5, [134, 268, 401, 535]),
(CommonVideoModel::Seedance2p0, [93, 185, 278, 370]),
(CommonVideoModel::Seedance2p0Fast, [64, 127, 191, 255]),
(CommonVideoModel::Seedance2p0Mini, [45, 89, 134, 178]),
(CommonVideoModel::Seedance2p5Ultra, [158, 316, 474, 632]),
(CommonVideoModel::Seedance2p0BytePlus, [125, 250, 375, 500]),
(CommonVideoModel::Seedance2p0BytePlusFast, [100, 200, 300, 400]),
(CommonVideoModel::Seedance2p0BytePlusMini, [46, 91, 137, 182]),
(CommonVideoModel::Seedance2p0BytePlusUltra, [125, 250, 375, 500]),
(CommonVideoModel::Seedance2p0BytePlusUltraFast, [100, 200, 300, 400]),
(CommonVideoModel::Seedance2p0BytePlusUltraMini, [46, 91, 137, 182]),
(CommonVideoModel::HappyHorse1p0, [85, 171, 256, 342]),
] {
for (index, price) in expected_prices.iter().copied().enumerate() {
let batch = index as u16 + 1;
let body = json!({
"model": model,
"duration_seconds": 5,
"resolution": "seven_twenty_p",
"video_batch_count": batch,
});
let api_request: OmniApiVideoGenerateRequest = serde_json::from_value(body.clone()).unwrap();
validate_video_request(&api_request).unwrap();
let converted_request = to_omni_gen_request(api_request);
let gen_request: OmniGenVideoCostAndGenerateRequest = serde_json::from_value(body).unwrap();
for request in [gen_request, converted_request] {
validate_omni_gen(&request).unwrap();
let builder = hydrate_to_router_request(&request).unwrap();
assert_eq!(builder.video_batch_count, Some(batch), "{model:?}");
assert!(matches!(builder.provider, RouterProvider::Artcraft));
let cost = builder.build2().unwrap().estimate_cost().unwrap();
assert_eq!(cost.cost_in_credits, Some(price), "{model:?}, batch {batch}");
assert_eq!(cost.cost_in_usd_cents, Some(price), "{model:?}, batch {batch}");
}
}
}
}
}
@@ -1,4 +1,5 @@
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::omni_gen::shared_utils::video::validate_kinovi_batch_count::validate_kinovi_batch_count;
use crate::http_server::endpoints::omni_api::shared_utils::video::validate_seedance_2p0_limits::validate_seedance_2p0_limits;
use crate::http_server::endpoints::omni_api::shared_utils::video::validate_when_image_required::validate_when_image_required;
use artcraft_api_defs::omni_api::generate_requests::omni_api_video_generate_request::OmniApiVideoGenerateRequest;
@@ -7,6 +8,7 @@ use artcraft_api_defs::omni_api::generate_requests::omni_api_video_generate_requ
pub fn validate_video_request(
request: &OmniApiVideoGenerateRequest,
) -> Result<(), CommonWebError> {
validate_kinovi_batch_count(request.model, request.video_batch_count)?;
validate_when_image_required(request)?;
validate_seedance_2p0_limits(request)?;
Ok(())
@@ -88,8 +88,8 @@ mod seedance_2p0_fast {
}
}
/// Every batch size 1-10 at every resolution (5s).
/// Batches over the platform max of 4 downgrade to 4 (3 upgrades to 4); execution and billing agree.
/// Every supported batch size 1-4 at every resolution (5s).
/// Batches 1-4 are preserved; larger batches are rejected before billing.
/// 1080p and 4K downgrade to 720p (and price accordingly).
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
@@ -101,42 +101,18 @@ mod seedance_2p0_fast {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(71)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(107)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(143)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(143)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(143)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(143)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(143)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(143)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(143)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(64)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(127)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(191)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(255)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(255)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(255)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(255)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(255)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(255)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(255)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(64)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(127)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(191)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(255)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(255)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(255)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(255)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(255)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(255)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(255)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(64)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(127)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(191)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(255)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(255)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(255)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(255)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(255)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(255)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(255)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -156,27 +132,15 @@ mod seedance_2p0_fast {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(86)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(150)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(257)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(314)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(371)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(428)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(153)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(267)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(458)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(560)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(662)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(764)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(153)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(267)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(458)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(560)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(662)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(764)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(153)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(267)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(458)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(560)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(662)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(764)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -252,8 +216,8 @@ mod seedance_2p0_fast {
}
/// With video references Fast switches to its with-references rate card (480p 8.41, 720p 15.60 cents/s, ceil-rounded). Charges are asserted on the refunded ledger entry (the fixture media is unreachable).
/// Every batch size 1-10 at every resolution (5s).
/// Batches over the platform max of 4 downgrade to 4 (3 upgrades to 4); execution and billing agree.
/// Every supported batch size 1-4 at every resolution (5s).
/// Batches 1-4 are preserved; larger batches are rejected before billing.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_every_batch_size_at_every_resolution() {
@@ -264,42 +228,18 @@ mod seedance_2p0_fast {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(85)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(127)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(169)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(169)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(169)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(169)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(169)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(169)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(169)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(78)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(156)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(234)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(312)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(312)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(312)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(312)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(312)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(312)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(312)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(78)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(156)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(234)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(312)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(312)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(312)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(312)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(312)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(312)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(312)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(78)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(156)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(234)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(312)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(312)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(312)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(312)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(312)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(312)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(312)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -336,27 +276,15 @@ mod seedance_2p0_fast {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(101)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(177)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(303)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(371)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(438)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(505)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(188)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(328)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(562)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(687)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(812)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(936)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(188)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(328)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(562)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(687)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(812)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(936)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(188)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(328)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(562)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(687)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(812)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(936)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -438,8 +366,8 @@ mod seedance_2p0_bp_fast {
}
}
/// Every batch size 1-10 at every resolution (5s).
/// Batches over the platform max of 4 downgrade to 4 (3 upgrades to 4); execution and billing agree.
/// Every supported batch size 1-4 at every resolution (5s).
/// Batches 1-4 are preserved; larger batches are rejected before billing.
/// 1080p and 4K downgrade to 720p (and price accordingly).
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
@@ -451,42 +379,18 @@ mod seedance_2p0_bp_fast {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(90)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(135)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(180)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(100)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(200)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(300)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(400)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(400)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(400)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(400)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(400)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(400)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(100)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(200)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(300)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(100)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(200)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(300)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(400)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -506,27 +410,15 @@ mod seedance_2p0_bp_fast {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(108)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(189)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(324)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(396)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(468)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(540)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(240)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(420)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(720)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(880)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(1040)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1200)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(240)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(420)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(720)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(880)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1040)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1200)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(240)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(420)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(720)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(880)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1040)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1200)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -602,8 +494,8 @@ mod seedance_2p0_bp_fast {
}
/// The BytePlus Fast rate card is flat: reference videos never change the price. Charges are asserted on the refunded ledger entry (the fixture media is unreachable).
/// Every batch size 1-10 at every resolution (5s).
/// Batches over the platform max of 4 downgrade to 4 (3 upgrades to 4); execution and billing agree.
/// Every supported batch size 1-4 at every resolution (5s).
/// Batches 1-4 are preserved; larger batches are rejected before billing.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_every_batch_size_at_every_resolution() {
@@ -614,42 +506,18 @@ mod seedance_2p0_bp_fast {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(97)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(145)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(193)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(193)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(193)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(193)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(193)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(193)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(193)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(103)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(205)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(308)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(410)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(410)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(410)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(410)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(410)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(410)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(103)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(205)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(308)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(103)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(205)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(308)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(410)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -686,27 +554,15 @@ mod seedance_2p0_bp_fast {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(116)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(203)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(348)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(425)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(502)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(579)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(246)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(431)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(738)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(902)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(1066)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1230)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(246)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(431)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(738)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(902)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1066)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1230)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(246)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(431)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(738)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(902)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1066)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1230)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -804,8 +660,8 @@ mod seedance_2p0_bpu_fast {
}
}
/// Every batch size 1-10 at every resolution (5s).
/// Batches over the platform max of 4 downgrade to 4 (3 upgrades to 4); execution and billing agree.
/// Every supported batch size 1-4 at every resolution (5s).
/// Batches 1-4 are preserved; larger batches are rejected before billing.
/// 1080p and 4K downgrade to 720p (and price accordingly).
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
@@ -817,42 +673,18 @@ mod seedance_2p0_bpu_fast {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(90)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(135)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(180)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(100)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(200)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(300)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(400)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(400)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(400)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(400)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(400)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(400)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(100)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(200)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(300)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(400)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(100)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(200)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(300)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(400)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(400)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -872,27 +704,15 @@ mod seedance_2p0_bpu_fast {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(108)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(189)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(324)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(396)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(468)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(540)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(240)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(420)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(720)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(880)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(1040)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1200)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(240)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(420)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(720)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(880)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1040)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1200)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(240)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(420)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(720)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(880)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1040)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1200)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -968,8 +788,8 @@ mod seedance_2p0_bpu_fast {
}
/// The BytePlus Fast rate card is flat: reference videos never change the price. Charges are asserted on the refunded ledger entry (the fixture media is unreachable).
/// Every batch size 1-10 at every resolution (5s).
/// Batches over the platform max of 4 downgrade to 4 (3 upgrades to 4); execution and billing agree.
/// Every supported batch size 1-4 at every resolution (5s).
/// Batches 1-4 are preserved; larger batches are rejected before billing.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_every_batch_size_at_every_resolution() {
@@ -980,42 +800,18 @@ mod seedance_2p0_bpu_fast {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(97)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(145)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(193)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(193)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(193)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(193)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(193)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(193)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(193)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(103)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(205)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(308)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(410)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(410)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(410)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(410)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(410)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(410)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(103)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(205)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(308)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(410)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(103)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(205)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(308)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(410)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(410)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -1052,27 +848,15 @@ mod seedance_2p0_bpu_fast {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(116)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(203)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(348)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(425)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(502)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(579)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(246)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(431)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(738)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(902)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(1066)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1230)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(246)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(431)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(738)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(902)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1066)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1230)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(246)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(431)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(738)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(902)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1066)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1230)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -12,7 +12,7 @@
//!
//! The Mini tier offers 480p and 720p; 1080p and 4K requests downgrade to
//! 720p on both execution and billing, which the tables below pin. Batches
//! of 1-8 bill in full (once, batch 8 executed in full but billed as 4).
//! of 1-4 bill in full; larger batches are rejected before billing.
use enums::common::generation::common_resolution::CommonResolution;
use enums::common::generation::common_video_model::CommonVideoModel;
@@ -91,8 +91,8 @@ mod seedance_2p0_mini {
}
}
/// Every batch size 1-10 at every resolution (5s).
/// Mini supports batches of 1-8 and bills them in full; 9-10 clamp to 8.
/// Every supported batch size 1-4 at every resolution (5s).
/// Mini supports batches of 1-4 and bills them in full.
/// 1080p and 4K downgrade to 720p (and price accordingly).
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
@@ -104,42 +104,18 @@ mod seedance_2p0_mini {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(35)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(52)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(69)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(87)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(104)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(121)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(138)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(138)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(138)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(45)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(89)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(178)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(223)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(267)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(312)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(356)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(356)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(356)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(45)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(89)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(134)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(178)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(223)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(267)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(312)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(356)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(356)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(356)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(45)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(89)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(134)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(178)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(223)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(267)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(312)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(356)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(356)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(356)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -159,27 +135,15 @@ mod seedance_2p0_mini {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(42)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(73)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(125)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(190)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(359)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(414)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(107)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(187)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(321)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(490)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(926)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1068)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(107)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(187)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(321)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(490)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(926)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1068)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(107)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(187)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(321)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(490)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(926)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1068)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -255,8 +219,8 @@ mod seedance_2p0_mini {
}
/// With video references Mini adds its per-second surcharge before the single ceil-rounding. Charges are asserted on the refunded ledger entry (the fixture media is unreachable).
/// Every batch size 1-10 at every resolution (5s).
/// Mini supports batches of 1-8 and bills them in full; 9-10 clamp to 8.
/// Every supported batch size 1-4 at every resolution (5s).
/// Mini supports batches of 1-4 and bills them in full.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_every_batch_size_at_every_resolution() {
@@ -267,42 +231,18 @@ mod seedance_2p0_mini {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(44)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(66)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(88)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(109)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(131)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(153)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(175)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(175)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(175)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(54)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(108)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(161)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(215)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(268)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(322)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(375)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(429)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(429)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(429)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(54)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(108)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(161)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(215)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(268)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(322)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(375)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(429)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(429)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(429)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(54)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(108)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(161)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(215)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(268)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(322)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(375)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(429)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(429)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(429)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -339,27 +279,15 @@ mod seedance_2p0_mini {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(53)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(92)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(157)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(240)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(453)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(523)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(129)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(225)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(386)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(589)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(1113)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1285)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(129)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(225)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(386)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(589)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1113)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1285)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(129)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(225)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(386)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(589)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1113)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1285)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -442,8 +370,8 @@ mod seedance_2p0_bp_mini {
}
}
/// Every batch size 1-10 at every resolution (5s).
/// Mini supports batches of 1-8 and bills them in full; 9-10 clamp to 8.
/// Every supported batch size 1-4 at every resolution (5s).
/// Mini supports batches of 1-4 and bills them in full.
/// 1080p and 4K downgrade to 720p (and price accordingly).
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
@@ -455,42 +383,18 @@ mod seedance_2p0_bp_mini {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(36)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(54)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(71)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(89)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(107)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(125)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(142)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(142)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(142)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(46)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(91)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(137)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(182)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(228)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(273)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(319)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(364)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(364)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(364)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(46)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(91)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(137)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(182)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(228)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(273)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(319)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(364)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(364)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(364)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(46)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(91)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(137)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(182)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(228)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(273)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(319)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(364)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(364)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(364)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -510,27 +414,15 @@ mod seedance_2p0_bp_mini {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(43)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(75)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(128)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(196)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(370)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(426)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(110)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(192)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(328)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(501)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(947)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1092)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(110)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(192)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(328)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(501)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(947)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1092)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(110)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(192)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(328)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(501)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(947)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1092)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -606,8 +498,8 @@ mod seedance_2p0_bp_mini {
}
/// With video references Mini adds its per-second surcharge before the single ceil-rounding. Charges are asserted on the refunded ledger entry (the fixture media is unreachable).
/// Every batch size 1-10 at every resolution (5s).
/// Mini supports batches of 1-8 and bills them in full; 9-10 clamp to 8.
/// Every supported batch size 1-4 at every resolution (5s).
/// Mini supports batches of 1-4 and bills them in full.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_every_batch_size_at_every_resolution() {
@@ -618,42 +510,18 @@ mod seedance_2p0_bp_mini {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(45)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(68)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(90)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(113)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(135)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(158)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(180)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(55)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(110)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(165)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(219)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(274)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(329)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(384)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(438)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(438)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(438)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(55)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(110)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(165)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(219)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(274)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(329)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(384)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(438)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(438)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(438)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(55)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(110)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(165)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(219)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(274)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(329)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(384)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(438)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(438)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(438)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -690,27 +558,15 @@ mod seedance_2p0_bp_mini {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(54)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(95)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(162)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(248)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(468)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(540)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(132)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(230)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(395)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(603)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(1139)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1314)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(132)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(395)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(603)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1139)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1314)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(132)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(395)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(603)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1139)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1314)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -791,8 +647,8 @@ mod seedance_2p0_bpu_mini {
}
}
/// Every batch size 1-10 at every resolution (5s).
/// Mini supports batches of 1-8 and bills them in full; 9-10 clamp to 8.
/// Every supported batch size 1-4 at every resolution (5s).
/// Mini supports batches of 1-4 and bills them in full.
/// 1080p and 4K downgrade to 720p (and price accordingly).
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
@@ -804,42 +660,18 @@ mod seedance_2p0_bpu_mini {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(36)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(54)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(71)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(89)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(107)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(125)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(142)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(142)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(142)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(46)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(91)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(137)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(182)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(228)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(273)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(319)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(364)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(364)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(364)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(46)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(91)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(137)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(182)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(228)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(273)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(319)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(364)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(364)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(364)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(46)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(91)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(137)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(182)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(228)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(273)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(319)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(364)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(364)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(364)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -859,27 +691,15 @@ mod seedance_2p0_bpu_mini {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(43)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(75)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(128)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(196)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(370)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(426)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(110)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(192)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(328)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(501)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(947)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1092)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(110)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(192)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(328)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(501)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(947)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1092)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(110)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(192)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(328)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(501)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(947)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1092)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -955,8 +775,8 @@ mod seedance_2p0_bpu_mini {
}
/// With video references Mini adds its per-second surcharge before the single ceil-rounding. Charges are asserted on the refunded ledger entry (the fixture media is unreachable).
/// Every batch size 1-10 at every resolution (5s).
/// Mini supports batches of 1-8 and bills them in full; 9-10 clamp to 8.
/// Every supported batch size 1-4 at every resolution (5s).
/// Mini supports batches of 1-4 and bills them in full.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_every_batch_size_at_every_resolution() {
@@ -967,42 +787,18 @@ mod seedance_2p0_bpu_mini {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(45)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(68)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(90)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(113)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(135)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(158)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(180)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(180)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(55)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(110)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(165)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(219)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(274)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(329)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(384)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(438)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(438)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(438)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(55)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(110)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(165)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(219)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(274)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(329)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(384)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(438)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(438)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(438)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(55)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(110)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(165)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(219)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(274)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(329)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(384)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(438)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(438)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(438)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -1039,27 +835,15 @@ mod seedance_2p0_bpu_mini {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(54)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(95)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(162)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(248)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(468)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(540)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(132)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(230)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(395)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(603)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(1139)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1314)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(132)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(395)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(603)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1139)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1314)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(132)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(395)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(603)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1139)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1314)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -1089,7 +873,6 @@ mod premium {
(Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(89), ExpectedCredits(91), CreditsDelta(2)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(134), ExpectedCredits(137), CreditsDelta(3)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(1), ExpectedCredits(52), ExpectedCredits(54), CreditsDelta(2)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(356), ExpectedCredits(364), CreditsDelta(8)),
];
for (resolution, seconds, batch, base, variant, delta) in cases {
@@ -1113,7 +896,6 @@ mod premium {
(Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(89), ExpectedCredits(91), CreditsDelta(2)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(134), ExpectedCredits(137), CreditsDelta(3)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(1), ExpectedCredits(52), ExpectedCredits(54), CreditsDelta(2)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(356), ExpectedCredits(364), CreditsDelta(8)),
];
for (resolution, seconds, batch, base, variant, delta) in cases {
@@ -91,8 +91,8 @@ mod seedance_2p0 {
}
}
/// Every batch size 1-10 at every resolution (5s).
/// Batches over the platform max of 4 downgrade to 4 (3 upgrades to 4); execution and billing agree.
/// Every supported batch size 1-4 at every resolution (5s).
/// Batches 1-4 are preserved; larger batches are rejected before billing.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn charges_every_batch_size_at_every_resolution() {
@@ -103,42 +103,18 @@ mod seedance_2p0 {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(78)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(117)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(155)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(155)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(155)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(155)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(155)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(155)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(155)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(93)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(185)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(278)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(370)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(370)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(370)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(370)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(370)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(370)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(370)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(233)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(466)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(699)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(933)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(933)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(933)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(933)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(933)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(933)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(933)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(463)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(925)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1388)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1850)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(1850)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(1850)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(1850)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(1850)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(1850)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(1850)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -158,27 +134,15 @@ mod seedance_2p0 {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(93)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(163)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(280)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(342)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(404)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(466)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(222)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(389)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(666)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(814)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(962)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1110)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(560)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(979)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1679)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(2052)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(2425)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(2798)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1110)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1943)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(3330)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(4070)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(4810)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(5550)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -254,8 +218,8 @@ mod seedance_2p0 {
}
/// With video references Seedance 2.0 switches to its with-references rate card (480p 8.81, 720p 22.05, 1080p 51.10, 4K 113.80 cents/s, ceil-rounded). The unreachable fixture media fails the upload after billing, so the exact charge is asserted on the refunded ledger entry.
/// Every batch size 1-10 at every resolution (5s).
/// Batches over the platform max of 4 downgrade to 4 (3 upgrades to 4); execution and billing agree.
/// Every supported batch size 1-4 at every resolution (5s).
/// Batches 1-4 are preserved; larger batches are rejected before billing.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_every_batch_size_at_every_resolution() {
@@ -266,42 +230,18 @@ mod seedance_2p0 {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(89)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(133)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(177)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(177)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(177)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(177)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(177)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(177)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(177)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(111)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(221)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(331)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(441)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(441)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(441)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(441)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(441)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(441)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(441)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(256)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(511)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(767)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1022)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1022)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1022)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1022)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1022)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1022)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1022)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(569)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(1138)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1707)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(2276)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(2276)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(2276)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(2276)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(2276)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(2276)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(2276)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -338,27 +278,15 @@ mod seedance_2p0 {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(106)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(186)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(318)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(388)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(459)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(529)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(265)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(464)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(794)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(971)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(1147)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1323)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(614)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1074)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1840)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(2249)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(2658)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(3066)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1366)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(2390)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(4097)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(5008)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(5918)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(6828)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -463,8 +391,8 @@ mod seedance_2p0_bp {
}
}
/// Every batch size 1-10 at every resolution (5s).
/// Batches over the platform max of 4 downgrade to 4 (3 upgrades to 4); execution and billing agree.
/// Every supported batch size 1-4 at every resolution (5s).
/// Batches 1-4 are preserved; larger batches are rejected before billing.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn charges_every_batch_size_at_every_resolution() {
@@ -475,42 +403,18 @@ mod seedance_2p0_bp {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(100)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(150)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(200)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(200)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(200)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(200)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(200)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(200)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(200)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(125)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(250)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(375)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(500)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(500)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(500)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(500)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(500)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(500)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(500)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(250)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(500)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(750)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1000)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1000)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1000)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1000)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1000)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1000)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1000)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(475)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(950)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1425)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1900)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(1900)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(1900)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(1900)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(1900)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(1900)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(1900)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -530,27 +434,15 @@ mod seedance_2p0_bp {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(120)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(210)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(360)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(440)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(520)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(600)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(300)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(525)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(900)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(1100)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(1300)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1500)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(600)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1050)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1800)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(2200)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(2600)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(3000)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1140)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1995)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(3420)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(4180)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(4940)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(5700)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -626,8 +518,8 @@ mod seedance_2p0_bp {
}
/// The BytePlus rate cards are flat below 4K: reference videos only change the 4K price (a 23.00 cents/s surcharge). Charges are asserted on the refunded ledger entry (the fixture media is unreachable).
/// Every batch size 1-10 at every resolution (5s).
/// Batches over the platform max of 4 downgrade to 4 (3 upgrades to 4); execution and billing agree.
/// Every supported batch size 1-4 at every resolution (5s).
/// Batches 1-4 are preserved; larger batches are rejected before billing.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_every_batch_size_at_every_resolution() {
@@ -638,42 +530,18 @@ mod seedance_2p0_bp {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(102)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(153)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(204)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(204)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(204)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(204)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(204)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(204)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(204)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(129)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(257)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(386)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(514)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(514)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(514)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(514)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(514)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(514)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(514)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(289)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(578)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(867)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1156)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1156)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1156)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1156)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1156)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1156)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1156)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(595)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(1190)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1785)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(2380)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(2380)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(2380)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(2380)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(2380)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(2380)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(2380)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -710,27 +578,15 @@ mod seedance_2p0_bp {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(123)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(215)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(368)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(449)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(531)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(612)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(309)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(540)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(926)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(1131)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(1337)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1542)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(694)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1214)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(2081)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(2544)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(3006)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(3468)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1428)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(2499)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(4284)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(5236)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(6188)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(7140)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -828,8 +684,8 @@ mod seedance_2p0_bpu {
}
}
/// Every batch size 1-10 at every resolution (5s).
/// Batches over the platform max of 4 downgrade to 4 (3 upgrades to 4); execution and billing agree.
/// Every supported batch size 1-4 at every resolution (5s).
/// Batches 1-4 are preserved; larger batches are rejected before billing.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn charges_every_batch_size_at_every_resolution() {
@@ -840,42 +696,18 @@ mod seedance_2p0_bpu {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(100)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(150)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(200)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(200)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(200)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(200)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(200)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(200)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(200)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(125)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(250)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(375)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(500)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(500)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(500)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(500)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(500)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(500)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(500)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(250)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(500)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(750)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1000)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1000)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1000)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1000)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1000)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1000)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1000)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(475)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(950)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1425)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1900)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(1900)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(1900)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(1900)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(1900)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(1900)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(1900)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -895,27 +727,15 @@ mod seedance_2p0_bpu {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(120)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(210)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(360)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(440)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(520)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(600)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(300)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(525)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(900)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(1100)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(1300)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1500)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(600)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1050)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1800)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(2200)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(2600)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(3000)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1140)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1995)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(3420)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(4180)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(4940)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(5700)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -991,8 +811,8 @@ mod seedance_2p0_bpu {
}
/// The BytePlus rate cards are flat below 4K: reference videos only change the 4K price (a 23.00 cents/s surcharge). Charges are asserted on the refunded ledger entry (the fixture media is unreachable).
/// Every batch size 1-10 at every resolution (5s).
/// Batches over the platform max of 4 downgrade to 4 (3 upgrades to 4); execution and billing agree.
/// Every supported batch size 1-4 at every resolution (5s).
/// Batches 1-4 are preserved; larger batches are rejected before billing.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_every_batch_size_at_every_resolution() {
@@ -1003,42 +823,18 @@ mod seedance_2p0_bpu {
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(102)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(153)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(204)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(204)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(204)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(204)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(204)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(204)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(204)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(129)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(257)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(386)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(514)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(514)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(514)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(514)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(514)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(514)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(514)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(289)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(578)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(867)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1156)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1156)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1156)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1156)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1156)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1156)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1156)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(595)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(1190)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1785)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(2380)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(2380)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(2380)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(2380)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(2380)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(2380)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(2380)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -1075,27 +871,15 @@ mod seedance_2p0_bpu {
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(123)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(215)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(368)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(449)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(531)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(612)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(309)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(540)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(926)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(1131)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(1337)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(1542)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(694)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1214)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(2081)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(2544)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(3006)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(3468)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1428)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(2499)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(4284)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(5236)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(6188)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(7140)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -1146,7 +930,6 @@ mod premium {
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(370), ExpectedCredits(500), CreditsDelta(130)),
// Batch 8 is accepted but downgrades to the platform max of 4 on both
// execution and billing, so it prices identically to batch 4.
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(370), ExpectedCredits(500), CreditsDelta(130)),
// 4K premium: the base tier prices at 92.50 cents/s while the BytePlus
// tiers price at 95.0 cents/s.
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(463), ExpectedCredits(475), CreditsDelta(12)),
@@ -1177,7 +960,6 @@ mod premium {
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(370), ExpectedCredits(500), CreditsDelta(130)),
// Batch 8 is accepted but downgrades to the platform max of 4 on both
// execution and billing, so it prices identically to batch 4.
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(370), ExpectedCredits(500), CreditsDelta(130)),
// 4K premium: the base tier prices at 92.50 cents/s while the BytePlus
// tiers price at 95.0 cents/s.
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(463), ExpectedCredits(475), CreditsDelta(12)),
@@ -12,8 +12,7 @@
//! 720p 18.72427984 ¢/s, 1080p 41.84487580 ¢/s
//!
//! Both models offer 480p/720p/1080p (4K downgrades to 1080p), durations
//! run 4-30s, and every request generates a single video regardless of the
//! requested batch size.
//! run 4-30s, and batches of 1-4 bill the full batch.
use enums::common::generation::common_resolution::CommonResolution;
use enums::common::generation::common_video_model::CommonVideoModel;
@@ -153,8 +152,8 @@ mod seedance_2p5 {
}
}
/// Every batch size 1-10 at every resolution (5s).
/// Seedance 2.5 generates a single video per request: every requested batch size 1-10 bills exactly one video.
/// Every supported batch size 1-4 at every resolution (5s).
/// Seedance 2.5 bills every video in the batch, rounding once after multiplying.
/// Seedance 2.5 offers 480p, 720p, and 1080p; 4K downgrades to 1080p (and prices accordingly).
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
@@ -163,45 +162,21 @@ mod seedance_2p5 {
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(59)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(59)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(59)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(59)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(59)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(59)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(59)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(59)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(59)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(59)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(118)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(177)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(236)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(268)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(401)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(535)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(617)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(925)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1233)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(617)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(925)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1233)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -211,37 +186,25 @@ mod seedance_2p5 {
}
}
/// Spot checks across duration x batch x resolution (batch always bills as one video).
/// Spot checks across duration x batch x resolution.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn charges_spot_checked_combinations() {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(71)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(83)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(106)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(130)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(154)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(177)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(161)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(187)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(241)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(294)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(348)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(401)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(370)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(432)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(555)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(678)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(802)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(925)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(370)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(432)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(555)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(678)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(802)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(925)),
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(142)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(248)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(424)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(321)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(561)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(962)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(740)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1295)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(2219)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(740)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1295)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(2219)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -377,8 +340,8 @@ mod seedance_2p5 {
}
/// With video references the per-second rate drops but billed seconds = output duration + probed input seconds. The fixture media is unreachable, so the probe fails open to the worst-case 30 input seconds, the upload then fails, and the exact charge is asserted on the refunded ledger entry.
/// Every batch size 1-10 at every resolution (5s).
/// Seedance 2.5 generates a single video per request: every requested batch size 1-10 bills exactly one video.
/// Every supported batch size 1-4 at every resolution (5s).
/// Seedance 2.5 bills every video in the batch, rounding once after multiplying.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_every_batch_size_at_every_resolution() {
@@ -386,45 +349,21 @@ mod seedance_2p5 {
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(254)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(254)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(254)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(254)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(254)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(254)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(254)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(254)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(254)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(254)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(507)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(761)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(1014)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(1110)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(1664)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(2219)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(2695)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(4043)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(5390)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(2695)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(4043)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(5390)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -442,30 +381,18 @@ mod seedance_2p5 {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(261)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(268)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(283)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(297)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(312)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(326)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(571)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(587)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(618)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(650)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(682)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(713)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(1386)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1425)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1502)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(1579)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1656)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1733)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1386)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1425)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(1502)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(1579)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1656)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1733)),
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(522)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(804)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(1130)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(1141)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(1759)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(2472)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(2772)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(4274)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(6006)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(2772)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(4274)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(6006)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -609,8 +536,8 @@ mod seedance_2p5_u {
}
}
/// Every batch size 1-10 at every resolution (5s).
/// Seedance 2.5 generates a single video per request: every requested batch size 1-10 bills exactly one video.
/// Every supported batch size 1-4 at every resolution (5s).
/// Seedance 2.5 bills every video in the batch, rounding once after multiplying.
/// Seedance 2.5 Ultra offers 480p, 720p, and 1080p; 4K downgrades to 1080p (and prices accordingly).
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
@@ -619,45 +546,21 @@ mod seedance_2p5_u {
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(70)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(70)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(70)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(70)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(70)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(70)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(70)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(70)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(70)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(70)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(140)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(209)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(279)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(316)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(474)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(632)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(673)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(1009)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1345)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(673)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1009)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1345)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -667,37 +570,25 @@ mod seedance_2p5_u {
}
}
/// Spot checks across duration x batch x resolution (batch always bills as one video).
/// Spot checks across duration x batch x resolution.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn charges_spot_checked_combinations() {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(84)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(98)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(126)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(154)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(181)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(209)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(190)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(221)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(285)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(348)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(411)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(474)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(404)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(471)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(606)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(740)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(874)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1009)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(404)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(471)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(606)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(740)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(874)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1009)),
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(167)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(293)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(501)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(379)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(663)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(1137)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(807)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1412)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(2421)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(807)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1412)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(2421)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -833,8 +724,8 @@ mod seedance_2p5_u {
}
/// With video references the per-second rate drops but billed seconds = output duration + probed input seconds. The fixture media is unreachable, so the probe fails open to the worst-case 30 input seconds, the upload then fails, and the exact charge is asserted on the refunded ledger entry.
/// Every batch size 1-10 at every resolution (5s).
/// Seedance 2.5 generates a single video per request: every requested batch size 1-10 bills exactly one video.
/// Every supported batch size 1-4 at every resolution (5s).
/// Seedance 2.5 bills every video in the batch, rounding once after multiplying.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_every_batch_size_at_every_resolution() {
@@ -842,45 +733,21 @@ mod seedance_2p5_u {
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(300)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(300)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(300)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(300)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(300)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(300)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(300)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(300)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(300)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(300)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(600)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(899)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(1199)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(1311)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(1967)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(2622)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(2930)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(4394)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(5859)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(2930)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(4394)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(5859)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -898,30 +765,18 @@ mod seedance_2p5_u {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(309)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(317)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(334)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(351)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(369)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(386)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(675)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(693)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(731)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(768)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(806)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(843)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(1507)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1549)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1632)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(1716)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1800)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1884)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1507)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1549)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(1632)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(1716)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1800)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1884)),
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(617)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(951)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(1336)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(1349)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(2079)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(2921)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(3013)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(4645)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(6528)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(3013)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(4645)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(6528)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -1,3 +1,4 @@
pub(crate) mod validate_kinovi_batch_count;
mod validate_seedance_2p0_limits;
pub mod validate_video_request;
mod validate_when_image_required;
mod validate_when_image_required;
@@ -0,0 +1,99 @@
use enums::common::generation::common_video_model::CommonVideoModel;
use crate::http_server::common_responses::common_web_error::CommonWebError;
/// Keep the public batch limit aligned with ArtCraft billing before the
/// execution builder reaches Kinovi, whose Seedance API accepts larger batches.
pub(crate) fn validate_kinovi_batch_count(
model: Option<CommonVideoModel>,
video_batch_count: Option<u16>,
) -> Result<(), CommonWebError> {
if matches!(model, Some(
CommonVideoModel::Seedance2p5
| CommonVideoModel::Seedance2p0
| CommonVideoModel::Seedance2p0Fast
| CommonVideoModel::Seedance2p0Mini
| CommonVideoModel::Seedance2p5Ultra
| CommonVideoModel::Seedance2p0BytePlus
| CommonVideoModel::Seedance2p0BytePlusFast
| CommonVideoModel::Seedance2p0BytePlusMini
| CommonVideoModel::Seedance2p0BytePlusUltra
| CommonVideoModel::Seedance2p0BytePlusUltraFast
| CommonVideoModel::Seedance2p0BytePlusUltraMini
| CommonVideoModel::HappyHorse1p0
)) && !(1..=4).contains(&video_batch_count.unwrap_or(1)) {
return Err(CommonWebError::BadInputWithSimpleMessage(
"video_batch_count must be between 1 and 4 for this model".to_string(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use artcraft_api_defs::omni_api::generate_requests::omni_api_video_generate_request::OmniApiVideoGenerateRequest;
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
use serde_json::json;
use crate::http_server::endpoints::omni_api::shared_utils::video::validate_video_request::validate_video_request as validate_omni_api;
use crate::http_server::endpoints::omni_gen::shared_utils::video::validate_video_request::validate_video_request as validate_omni_gen;
use super::*;
const MODELS: [CommonVideoModel; 12] = [
CommonVideoModel::Seedance2p5,
CommonVideoModel::Seedance2p0,
CommonVideoModel::Seedance2p0Fast,
CommonVideoModel::Seedance2p0Mini,
CommonVideoModel::Seedance2p5Ultra,
CommonVideoModel::Seedance2p0BytePlus,
CommonVideoModel::Seedance2p0BytePlusFast,
CommonVideoModel::Seedance2p0BytePlusMini,
CommonVideoModel::Seedance2p0BytePlusUltra,
CommonVideoModel::Seedance2p0BytePlusUltraFast,
CommonVideoModel::Seedance2p0BytePlusUltraMini,
CommonVideoModel::HappyHorse1p0,
];
#[test]
fn both_endpoints_accept_batches_one_through_four_and_default() {
for model in MODELS {
for batch in [None, Some(1), Some(2), Some(3), Some(4)] {
for result in validate_both(model, batch) {
assert!(result.is_ok(), "{:?}, batch {:?}: {:?}", model, batch, result);
}
}
}
}
#[test]
fn both_endpoints_reject_zero_and_batches_above_four() {
for model in MODELS {
for batch in [0, 5, 6, 7, 8, u16::MAX] {
for result in validate_both(model, Some(batch)) {
assert!(matches!(result, Err(CommonWebError::BadInputWithSimpleMessage(_))),
"{:?}, batch {}", model, batch);
}
}
}
}
#[test]
fn other_models_keep_their_existing_validation() {
for model in [
CommonVideoModel::Seedance2p5Preview,
CommonVideoModel::Veo3p1,
] {
for result in validate_both(model, Some(8)) {
assert!(result.is_ok(), "{:?}: {:?}", model, result);
}
}
}
fn validate_both(model: CommonVideoModel, batch: Option<u16>) -> [Result<(), CommonWebError>; 2] {
let json = json!({"model": model, "video_batch_count": batch});
let omni_gen: OmniGenVideoCostAndGenerateRequest = serde_json::from_value(json.clone()).unwrap();
let omni_api: OmniApiVideoGenerateRequest = serde_json::from_value(json).unwrap();
[validate_omni_gen(&omni_gen), validate_omni_api(&omni_api)]
}
}
@@ -1,4 +1,5 @@
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::omni_gen::shared_utils::video::validate_kinovi_batch_count::validate_kinovi_batch_count;
use crate::http_server::endpoints::omni_gen::shared_utils::video::validate_seedance_2p0_limits::validate_seedance_2p0_limits;
use crate::http_server::endpoints::omni_gen::shared_utils::video::validate_when_image_required::validate_when_image_required;
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
@@ -7,6 +8,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost
pub fn validate_video_request(
request: &OmniGenVideoCostAndGenerateRequest,
) -> Result<(), CommonWebError> {
validate_kinovi_batch_count(request.model, request.video_batch_count)?;
validate_when_image_required(request)?;
validate_seedance_2p0_limits(request)?;
Ok(())