Compare commits

..
100 changed files with 2631 additions and 2038 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,
);
@@ -94,11 +97,11 @@ mod tests {
#[test]
fn table_1080p() {
// 45.85869386 ¢/s, rounded up.
assert_eq!(cents(Some(RouterResolution::TenEightyP), 4), 184);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 5), 230);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 10), 459);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 30), 1376);
// 61.62732357 ¢/s, rounded up.
assert_eq!(cents(Some(RouterResolution::TenEightyP), 4), 247);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 5), 309);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 10), 617);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 30), 1849);
}
#[test]
@@ -121,16 +124,16 @@ mod tests {
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(10)), 290);
// 15.84362140 ¢/s × 40 = 633.74 → 634¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::SevenTwentyP), 30, Some(10)), 634);
// 27.39973680 ¢/s × 40 = 1095.99 → 1096¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1096);
// 38.49728574 ¢/s × 40 = 1539.89 → 1540¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1540);
}
#[test]
fn table_1080p_with_ten_input_seconds() {
// 27.39973680 ¢/s over (output + 10) billed seconds, rounded up.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 5, Some(10)), 411);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 10, Some(10)), 548);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1096);
// 38.49728574 ¢/s over (output + 10) billed seconds, rounded up.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 5, Some(10)), 578);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 10, Some(10)), 770);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1540);
}
#[test]
@@ -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,
);
@@ -95,11 +98,11 @@ mod tests {
#[test]
fn table_1080p() {
// 50.10486922 ¢/s, rounded up.
assert_eq!(cents(Some(RouterResolution::TenEightyP), 4), 201);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 5), 251);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 10), 502);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 30), 1504);
// 67.22980753 ¢/s, rounded up.
assert_eq!(cents(Some(RouterResolution::TenEightyP), 4), 269);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 5), 337);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 10), 673);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 30), 2017);
}
#[test]
@@ -141,16 +144,16 @@ mod tests {
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(10)), 343);
// 18.72427984 ¢/s × 40 = 748.97 → 749¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::SevenTwentyP), 30, Some(10)), 749);
// 29.93674947 ¢/s × 40 = 1197.47 → 1198¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1198);
// 41.84487580 ¢/s × 40 = 1673.80 → 1674¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1674);
}
#[test]
fn table_1080p_with_ten_input_seconds() {
// 29.93674947 ¢/s over (output + 10) billed seconds, rounded up.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 5, Some(10)), 450);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 10, Some(10)), 599);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1198);
// 41.84487580 ¢/s over (output + 10) billed seconds, rounded up.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 5, Some(10)), 628);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 10, Some(10)), 837);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1674);
}
#[test]
@@ -137,14 +137,15 @@ pub fn seedance_2p5_preview_usd_cents(
const SEEDANCE_2P5_CENTS_PER_SECOND_480P: f64 = 11.76954733;
/// Seedance 2.5 — 720p price, USD cents per second.
const SEEDANCE_2P5_CENTS_PER_SECOND_720P: f64 = 26.70781893;
/// Seedance 2.5 — 1080p price, USD cents per second.
const SEEDANCE_2P5_CENTS_PER_SECOND_1080P: f64 = 45.85869386;
/// Seedance 2.5 — 1080p price, USD cents per second (updated 2026-09-17).
const SEEDANCE_2P5_CENTS_PER_SECOND_1080P: f64 = 61.62732357;
/// Seedance 2.5 — 480p price with video references, USD cents per second.
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_480P: f64 = 7.24279835;
/// Seedance 2.5 — 720p price with video references, USD cents per second.
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_720P: f64 = 15.84362140;
/// Seedance 2.5 — 1080p price with video references, USD cents per second.
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 27.39973680;
/// Seedance 2.5 — 1080p price with video references, USD cents per second
/// (updated 2026-09-17).
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 38.49728574;
/// ArtCraft's price (USD cents) for Seedance 2.5.
///
@@ -154,10 +155,11 @@ const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 27.39973680;
/// 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 {
@@ -192,21 +194,22 @@ 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.
const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_480P: f64 = 13.90946502;
/// Seedance 2.5 Ultra — 720p price, USD cents per second.
const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_720P: f64 = 31.56378601;
/// Seedance 2.5 Ultra — 1080p price, USD cents per second.
const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_1080P: f64 = 50.10486922;
/// Seedance 2.5 Ultra — 1080p price, USD cents per second (updated 2026-09-17).
const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_1080P: f64 = 67.22980753;
/// Seedance 2.5 Ultra — 480p price with video references, USD cents per second.
const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_480P: f64 = 8.55967078;
/// Seedance 2.5 Ultra — 720p price with video references, USD cents per second.
const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_720P: f64 = 18.72427984;
/// Seedance 2.5 Ultra — 1080p price with video references, USD cents per second.
const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 29.93674947;
/// Seedance 2.5 Ultra — 1080p price with video references, USD cents per
/// second (updated 2026-09-17).
const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 41.84487580;
/// ArtCraft's price (USD cents) for Seedance 2.5 Ultra.
///
@@ -215,11 +218,12 @@ const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 29.936749
/// 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 {
@@ -254,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,
@@ -100,7 +104,7 @@ impl KinoviSeedance2p5CostState {
// per-model credit rate at our bulk credit purchase rate).
let costs = pricing_request.calculate_enterprise_costs();
// 2.5 bills whole credits at 480p/720p, so the rounding below is exact
// there; 1080p bills fractional credits (103.25/sec) and rounds to the
// there; 1080p bills fractional credits (136.23/sec) and rounds to the
// nearest credit. The USD cents (the authoritative charge) are rounded
// up.
let cost_in_credits = costs.kinovi_credits.round() as u64;
@@ -125,7 +129,7 @@ mod tests {
use super::*;
// ── Credits without video references (26/sec at 480p, 59/sec at 720p,
// 103.25/sec at 1080p enterprise) ──
// 136.23/sec at 1080p enterprise) ──
mod credits_without_video_references {
use super::*;
@@ -145,11 +149,11 @@ mod tests {
#[test]
fn credits_1080p() {
// 103.25/sec enterprise; fractional totals round to the nearest credit
// (516.25 → 516, 1032.5 → 1033, 3097.5 → 3098).
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 5, false, None), 516);
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 10, false, None), 1033);
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, false, None), 3098);
// 136.23/sec enterprise; fractional totals round to the nearest credit
// (681.15 → 681, 1362.3 → 1362, 4086.9 → 4087).
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 5, false, None), 681);
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 10, false, None), 1362);
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, false, None), 4087);
}
#[test]
@@ -164,7 +168,7 @@ mod tests {
}
// ── Credits with video references (16/sec at 480p, 35/sec at 720p,
// 61.69/sec at 1080p enterprise, over output duration + input seconds) ──
// 81.4/sec at 1080p enterprise, over output duration + input seconds) ──
mod credits_with_video_references {
use super::*;
@@ -173,8 +177,8 @@ mod tests {
fn thirty_second_output_with_ten_input_seconds_bills_forty() {
assert_eq!(credits(Some(KinoviOutputResolution::FourEightyP), 30, true, Some(10)), 16 * 40);
assert_eq!(credits(Some(KinoviOutputResolution::SevenTwentyP), 30, true, Some(10)), 35 * 40);
// 61.69/sec enterprise × 40 = 2467.6 → 2468 credits.
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, true, Some(10)), 2468);
// 81.4/sec enterprise × 40 = 3256 credits.
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, true, Some(10)), 3256);
}
#[test]
@@ -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,
@@ -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
@@ -147,7 +161,7 @@ pub enum KinoviSeedance2p5OutputFormat {
// |------------|----------------------|------------------------|
// | 480p | 26 | (no discount) |
// | 720p | 59 | (no discount) |
// | 1080p | 107.55 | 103.25 |
// | 1080p | 143.4 | 136.23 |
//
// With video references, the rate drops but the billed seconds are the
// output duration PLUS the total seconds of reference video input
@@ -160,11 +174,10 @@ pub enum KinoviSeedance2p5OutputFormat {
// |------------|----------------------|------------------------|
// | 480p | 16 | (no discount) |
// | 720p | 35 | (no discount) |
// | 1080p | 64.26 | 61.69 |
// | 1080p | 85.68 | 81.4 |
//
// The 1080p enterprise rates were spot-checked against the Kinovi site
// (2026-08-17): 4s / 10s / 30s without a reference video showed 413 /
// 1,032.5 / 3,097.5 credits. 480p and 720p have no separately negotiated
// The 1080p rates were last updated 2026-09-17 (previously 107.55 / 103.25
// and 64.26 / 61.69 credits/sec). 480p and 720p have no separately negotiated
// enterprise credit rate, so enterprise bills the same credits as consumer
// (the tiers still convert to USD at their own purchase rates). Default
// resolution (None) is 720p.
@@ -180,8 +193,8 @@ const SEEDANCE_2P5_720P: KinoviPricingRate = KinoviPricingRate {
maybe_enterprise_credits: None,
};
const SEEDANCE_2P5_1080P: KinoviPricingRate = KinoviPricingRate {
consumer_credits: 107.55,
maybe_enterprise_credits: Some(103.25),
consumer_credits: 143.4,
maybe_enterprise_credits: Some(136.23),
};
/// Per-second credit rates when video references are attached. These REPLACE
@@ -196,8 +209,8 @@ const SEEDANCE_2P5_720P_VIDEO_REF: KinoviPricingRate = KinoviPricingRate {
maybe_enterprise_credits: None,
};
const SEEDANCE_2P5_1080P_VIDEO_REF: KinoviPricingRate = KinoviPricingRate {
consumer_credits: 64.26,
maybe_enterprise_credits: Some(61.69),
consumer_credits: 85.68,
maybe_enterprise_credits: Some(81.4),
};
impl KinoviCostCalculatorTrait for GenerateSeedance2p5Request {
@@ -234,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)
}
}
@@ -309,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,
@@ -355,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)]
@@ -375,7 +412,7 @@ mod tests {
// ── No video references: output-duration billing ──
//
// 480p = 26 credits/sec and 720p = 59 credits/sec at both tiers (no
// negotiated enterprise discount); 1080p = 107.55 consumer / 103.25
// negotiated enterprise discount); 1080p = 143.4 consumer / 136.23
// enterprise credits/sec.
mod without_video_references {
@@ -401,24 +438,23 @@ mod tests {
#[test]
fn table_1080p() {
// Enterprise 103.25 credits/sec — the 4s / 10s / 30s values were
// verified against the Kinovi site (413 / 1,032.5 / 3,097.5).
assert_eq!(t2v_1080(4).calculate_enterprise_costs().kinovi_credits, 413.0);
assert_eq!(t2v_1080(5).calculate_enterprise_costs().kinovi_credits, 516.25);
assert_eq!(t2v_1080(10).calculate_enterprise_costs().kinovi_credits, 1032.5);
assert_eq!(t2v_1080(15).calculate_enterprise_costs().kinovi_credits, 1548.75);
assert_eq!(t2v_1080(20).calculate_enterprise_costs().kinovi_credits, 2065.0);
assert_eq!(t2v_1080(25).calculate_enterprise_costs().kinovi_credits, 2581.25);
assert_eq!(t2v_1080(30).calculate_enterprise_costs().kinovi_credits, 3097.5);
// Enterprise 136.23 credits/sec (2026-09-17).
assert_eq!(t2v_1080(4).calculate_enterprise_costs().kinovi_credits, 544.92);
assert_eq!(t2v_1080(5).calculate_enterprise_costs().kinovi_credits, 681.15);
assert_eq!(t2v_1080(10).calculate_enterprise_costs().kinovi_credits, 1362.3);
assert_eq!(t2v_1080(15).calculate_enterprise_costs().kinovi_credits, 2043.45);
assert_eq!(t2v_1080(20).calculate_enterprise_costs().kinovi_credits, 2724.6);
assert_eq!(t2v_1080(25).calculate_enterprise_costs().kinovi_credits, 3405.75);
assert_eq!(t2v_1080(30).calculate_enterprise_costs().kinovi_credits, 4086.9);
// Consumer 107.55 credits/sec.
assert_eq!(t2v_1080(4).calculate_consumer_costs().kinovi_credits, 430.2);
assert_eq!(t2v_1080(5).calculate_consumer_costs().kinovi_credits, 537.75);
assert_eq!(t2v_1080(10).calculate_consumer_costs().kinovi_credits, 1075.5);
assert_eq!(t2v_1080(15).calculate_consumer_costs().kinovi_credits, 1613.25);
assert_eq!(t2v_1080(20).calculate_consumer_costs().kinovi_credits, 2151.0);
assert_eq!(t2v_1080(25).calculate_consumer_costs().kinovi_credits, 2688.75);
assert_eq!(t2v_1080(30).calculate_consumer_costs().kinovi_credits, 3226.5);
// Consumer 143.4 credits/sec.
assert_eq!(t2v_1080(4).calculate_consumer_costs().kinovi_credits, 573.6);
assert_eq!(t2v_1080(5).calculate_consumer_costs().kinovi_credits, 717.0);
assert_eq!(t2v_1080(10).calculate_consumer_costs().kinovi_credits, 1434.0);
assert_eq!(t2v_1080(15).calculate_consumer_costs().kinovi_credits, 2151.0);
assert_eq!(t2v_1080(20).calculate_consumer_costs().kinovi_credits, 2868.0);
assert_eq!(t2v_1080(25).calculate_consumer_costs().kinovi_credits, 3585.0);
assert_eq!(t2v_1080(30).calculate_consumer_costs().kinovi_credits, 4302.0);
}
#[test]
@@ -456,7 +492,7 @@ mod tests {
let mut request_1080 = keyframe_request(10);
request_1080.output_resolution = Some(KinoviSeedance2p5OutputResolution::TenEightyP);
assert_eq!(request_1080.calculate_enterprise_costs().kinovi_credits, 1032.5);
assert_eq!(request_1080.calculate_enterprise_costs().kinovi_credits, 1362.3);
}
#[test]
@@ -489,7 +525,7 @@ mod tests {
// ── With video references: input seconds are billed too ──
//
// 480p = 16 credits/sec and 720p = 35 credits/sec at both tiers; 1080p
// = 64.26 consumer / 61.69 enterprise credits/sec. All over
// = 85.68 consumer / 81.4 enterprise credits/sec. All over
// (output duration + total_input_seconds) billed seconds.
mod with_video_references {
@@ -541,21 +577,21 @@ mod tests {
// seconds: billed seconds = 20 + clamp(input, 4, 30). E.g. a 10s
// input video + 20s generation = 30 billed seconds.
//
// Enterprise 61.69 credits/sec.
assert_eq!(video_ref_1080(20, Some(1)).calculate_enterprise_costs().kinovi_credits, 1480.56);
assert_eq!(video_ref_1080(20, Some(5)).calculate_enterprise_costs().kinovi_credits, 1542.25);
assert_eq!(video_ref_1080(20, Some(10)).calculate_enterprise_costs().kinovi_credits, 1850.7);
assert_eq!(video_ref_1080(20, Some(15)).calculate_enterprise_costs().kinovi_credits, 2159.15);
assert_eq!(video_ref_1080(20, Some(20)).calculate_enterprise_costs().kinovi_credits, 2467.6);
assert_eq!(video_ref_1080(20, Some(30)).calculate_enterprise_costs().kinovi_credits, 3084.5);
// Enterprise 81.4 credits/sec.
assert_eq!(video_ref_1080(20, Some(1)).calculate_enterprise_costs().kinovi_credits, 1953.6);
assert_eq!(video_ref_1080(20, Some(5)).calculate_enterprise_costs().kinovi_credits, 2035.0);
assert_eq!(video_ref_1080(20, Some(10)).calculate_enterprise_costs().kinovi_credits, 2442.0);
assert_eq!(video_ref_1080(20, Some(15)).calculate_enterprise_costs().kinovi_credits, 2849.0);
assert_eq!(video_ref_1080(20, Some(20)).calculate_enterprise_costs().kinovi_credits, 3256.0);
assert_eq!(video_ref_1080(20, Some(30)).calculate_enterprise_costs().kinovi_credits, 4070.0);
// Consumer 64.26 credits/sec.
assert_eq!(video_ref_1080(20, Some(1)).calculate_consumer_costs().kinovi_credits, 1542.24);
assert_eq!(video_ref_1080(20, Some(5)).calculate_consumer_costs().kinovi_credits, 1606.5);
assert_eq!(video_ref_1080(20, Some(10)).calculate_consumer_costs().kinovi_credits, 1927.8);
assert_eq!(video_ref_1080(20, Some(15)).calculate_consumer_costs().kinovi_credits, 2249.1);
assert_eq!(video_ref_1080(20, Some(20)).calculate_consumer_costs().kinovi_credits, 2570.4);
assert_eq!(video_ref_1080(20, Some(30)).calculate_consumer_costs().kinovi_credits, 3213.0);
// Consumer 85.68 credits/sec.
assert_eq!(video_ref_1080(20, Some(1)).calculate_consumer_costs().kinovi_credits, 2056.32);
assert_eq!(video_ref_1080(20, Some(5)).calculate_consumer_costs().kinovi_credits, 2142.0);
assert_eq!(video_ref_1080(20, Some(10)).calculate_consumer_costs().kinovi_credits, 2570.4);
assert_eq!(video_ref_1080(20, Some(15)).calculate_consumer_costs().kinovi_credits, 2998.8);
assert_eq!(video_ref_1080(20, Some(20)).calculate_consumer_costs().kinovi_credits, 3427.2);
assert_eq!(video_ref_1080(20, Some(30)).calculate_consumer_costs().kinovi_credits, 4284.0);
}
#[test]
@@ -574,9 +610,9 @@ mod tests {
// 30-second maximum so the estimate never undershoots the charge.
assert_eq!(video_ref_480(10, None).calculate_consumer_costs().kinovi_credits, 640.0);
assert_eq!(video_ref_720(10, None).calculate_consumer_costs().kinovi_credits, 1400.0);
// 1080p: 40 billed seconds at 61.69 enterprise / 64.26 consumer.
assert_eq!(video_ref_1080(10, None).calculate_enterprise_costs().kinovi_credits, 2467.6);
assert_eq!(video_ref_1080(10, None).calculate_consumer_costs().kinovi_credits, 2570.4);
// 1080p: 40 billed seconds at 81.4 enterprise / 85.68 consumer.
assert_eq!(video_ref_1080(10, None).calculate_enterprise_costs().kinovi_credits, 3256.0);
assert_eq!(video_ref_1080(10, None).calculate_consumer_costs().kinovi_credits, 3427.2);
}
#[test]
@@ -649,47 +685,151 @@ mod tests {
#[test]
fn enterprise_cents_1080p_4s() {
// 413 credits; 41300/243.16 = 169.8470 cents.
// 544.92 credits; 54492/243.16 = 224.0994 cents.
let cost = t2v_1080(4).calculate_enterprise_costs();
assert_eq!(cost.usd_cents_rounded_up, 170);
assert_eq!(cost.usd_cents_rounded_down, 169);
assert!((cost.usd_cents_fractional - 169.8470).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 225);
assert_eq!(cost.usd_cents_rounded_down, 224);
assert!((cost.usd_cents_fractional - 224.0994).abs() < FLOAT_TOLERANCE);
}
#[test]
fn consumer_cents_1080p_4s() {
// 430.2 credits; 43020/192.98 = 222.9247 cents.
// 573.6 credits; 57360/192.98 = 297.2329 cents.
let cost = t2v_1080(4).calculate_consumer_costs();
assert_eq!(cost.usd_cents_rounded_up, 223);
assert_eq!(cost.usd_cents_rounded_down, 222);
assert!((cost.usd_cents_fractional - 222.9247).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 298);
assert_eq!(cost.usd_cents_rounded_down, 297);
assert!((cost.usd_cents_fractional - 297.2329).abs() < FLOAT_TOLERANCE);
}
#[test]
fn enterprise_cents_1080p_30s() {
// 3097.5 credits; 309750/243.16 = 1273.8526 cents.
// 4086.9 credits; 408690/243.16 = 1680.7452 cents.
let cost = t2v_1080(30).calculate_enterprise_costs();
assert_eq!(cost.usd_cents_rounded_up, 1274);
assert_eq!(cost.usd_cents_rounded_down, 1273);
assert!((cost.usd_cents_fractional - 1273.8526).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 1681);
assert_eq!(cost.usd_cents_rounded_down, 1680);
assert!((cost.usd_cents_fractional - 1680.7452).abs() < FLOAT_TOLERANCE);
}
#[test]
fn enterprise_cents_1080p_video_ref_30_billed_seconds() {
// 61.69 × 30 = 1850.7 credits; 185070/243.16 = 761.1038 cents.
// 81.4 × 30 = 2442 credits; 244200/243.16 = 1004.2770 cents.
let cost = video_ref_1080(20, Some(10)).calculate_enterprise_costs();
assert_eq!(cost.usd_cents_rounded_up, 762);
assert_eq!(cost.usd_cents_rounded_down, 761);
assert!((cost.usd_cents_fractional - 761.1038).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 1005);
assert_eq!(cost.usd_cents_rounded_down, 1004);
assert!((cost.usd_cents_fractional - 1004.2770).abs() < FLOAT_TOLERANCE);
}
#[test]
fn consumer_cents_1080p_video_ref_30_billed_seconds() {
// 64.26 × 30 = 1927.8 credits; 192780/192.98 = 998.9636 cents.
// 85.68 × 30 = 2570.4 credits; 257040/192.98 = 1331.9515 cents.
let cost = video_ref_1080(20, Some(10)).calculate_consumer_costs();
assert_eq!(cost.usd_cents_rounded_up, 999);
assert_eq!(cost.usd_cents_rounded_down, 998);
assert!((cost.usd_cents_fractional - 998.9636).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 1332);
assert_eq!(cost.usd_cents_rounded_down, 1331);
assert!((cost.usd_cents_fractional - 1331.9515).abs() < FLOAT_TOLERANCE);
}
}
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:?}",
);
}
}
}
}
@@ -697,10 +837,81 @@ mod tests {
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() {
// 16 < 26, 35 < 59, and 61.69 < 103.25: the with-references rate is
// 16 < 26, 35 < 59, and 81.4 < 136.23: the with-references rate is
// lower per billed second (the input seconds are where the money
// goes). Compare at the minimum billed input (1s clamps to 4) —
// unknown or zero input assumes the 30-second maximum, which would
@@ -771,6 +982,7 @@ mod tests {
output_resolution: Option<KinoviSeedance2p5OutputResolution>,
) -> GenerateSeedance2p5Request {
GenerateSeedance2p5Request {
batch_count: None,
prompt: String::new(),
modality: KinoviSeedance2p5Modality::Reference {
aspect_ratio: None,
@@ -802,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(),
@@ -864,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(),
@@ -890,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),
@@ -920,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,
@@ -942,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,
@@ -974,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),
@@ -1008,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),
@@ -1040,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(),
@@ -1070,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(),
@@ -1100,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,
@@ -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(())
@@ -75,7 +75,10 @@ pub fn hydrate_to_router_request(
},
duration_seconds: request.duration_seconds,
video_batch_count: request.video_batch_count,
generate_audio: request.generate_audio,
// Seedance 2.0 / 2.5 always generate sound. Kinovi honours an explicit
// `generate_audio: false`, and the frontend was sending one, so the flag
// is dropped here and never reaches Kinovi for these models.
generate_audio: if is_seedance_2x(&model) { None } else { request.generate_audio },
// Set by the handler after probing reference video durations (only
// models that bill input seconds use it).
total_reference_video_input_seconds: None,
@@ -84,6 +87,24 @@ pub fn hydrate_to_router_request(
})
}
fn is_seedance_2x(model: &RouterVideoModel) -> bool {
matches!(
model,
RouterVideoModel::Seedance2p0
| RouterVideoModel::Seedance2p0Fast
| RouterVideoModel::Seedance2p0Mini
| RouterVideoModel::Seedance2p0BytePlus
| RouterVideoModel::Seedance2p0BytePlusFast
| RouterVideoModel::Seedance2p0BytePlusMini
| RouterVideoModel::Seedance2p0BytePlusUltra
| RouterVideoModel::Seedance2p0BytePlusUltraFast
| RouterVideoModel::Seedance2p0BytePlusUltraMini
| RouterVideoModel::Seedance2p5Preview
| RouterVideoModel::Seedance2p5
| RouterVideoModel::Seedance2p5Ultra
)
}
fn convert_model(
model: &CommonVideoModelEnum,
) -> Result<RouterVideoModel, CommonWebError> {
@@ -144,7 +165,7 @@ mod tests {
use serde_json::{json, Value};
#[tokio::test]
async fn artcraft_http_request_reaches_kinovi_with_format_and_audio() {
async fn artcraft_http_request_reaches_kinovi_with_format_and_without_audio_flag() {
// No media inputs or send: finalization needs a client but makes no HTTP calls.
let client = RouterClient::KinoviWeb(RouterKinoviWebClient::new(
KinoviWebSession::from_cookies_string(String::new()),
@@ -183,7 +204,7 @@ mod tests {
});
let expected_format = output_format.as_ref().and_then(Value::as_str).unwrap_or("mp4");
assert_eq!(actual_format, Some(expected_format), "model: {model}");
assert_eq!(request.maybe_generate_audio, generate_audio);
assert_eq!(request.maybe_generate_audio, None, "generate_audio: {generate_audio:?}");
}
}
}
@@ -210,6 +231,44 @@ mod tests {
}
}
mod generate_audio {
use super::*;
#[test]
fn seedance_2x_drops_generate_audio() {
for model in [
CommonVideoModelEnum::Seedance2p0,
CommonVideoModelEnum::Seedance2p0Fast,
CommonVideoModelEnum::Seedance2p0Mini,
CommonVideoModelEnum::Seedance2p5,
CommonVideoModelEnum::Seedance2p5Ultra,
] {
for generate_audio in [None, Some(true), Some(false)] {
let request = OmniGenVideoCostAndGenerateRequest {
model: Some(model),
generate_audio,
..base_request()
};
let builder = hydrate_to_router_request(&request).expect("hydrate should succeed");
assert_eq!(builder.generate_audio, None, "model: {model:?}, generate_audio: {generate_audio:?}");
}
}
}
#[test]
fn other_models_keep_generate_audio() {
for generate_audio in [None, Some(true), Some(false)] {
let request = OmniGenVideoCostAndGenerateRequest {
model: Some(CommonVideoModelEnum::Veo3p1),
generate_audio,
..base_request()
};
let builder = hydrate_to_router_request(&request).expect("hydrate should succeed");
assert_eq!(builder.generate_audio, generate_audio);
}
}
}
mod bitrate_hydration {
use super::*;
@@ -12,8 +12,8 @@ use enums::common::generation::common_video_model::CommonVideoModel;
use artcraft_api_keys::ArtcraftApiKey;
use crate::http_server::endpoints::omni_gen::generate::video::tests::support::{
base_generate_request, to_omni_api_request, ExpectedCredits, Seconds, TestHarness,
STARTING_CREDITS,
base_generate_request, stub_traffic, to_omni_api_request, ExpectedCredits, Seconds,
TestHarness, STARTING_CREDITS,
};
mod billing_parity {
@@ -46,6 +46,26 @@ mod billing_parity {
}
}
mod generate_audio {
use super::*;
/// Seedance 2.0 / 2.5 always produce sound. Even when a caller explicitly
/// sends `generate_audio: false`, neither endpoint forwards the flag to
/// Kinovi — the key must be absent from the captured request body so Kinovi
/// falls back to its default of sound on.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn seedance_2p0_never_forwards_generate_audio_on_either_endpoint() {
assert_endpoints_omit_generate_audio(CommonVideoModel::Seedance2p0).await;
}
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn seedance_2p5_never_forwards_generate_audio_on_either_endpoint() {
assert_endpoints_omit_generate_audio(CommonVideoModel::Seedance2p5).await;
}
}
mod url_ingestion {
use super::*;
@@ -195,3 +215,47 @@ async fn assert_endpoints_bill_identically(
"{:?}: omni_api debited the wrong amount", model,
);
}
/// Run `model` with an explicit `generate_audio: false` through omni_gen
/// (session cookie) and omni_api (API key); assert both succeed and neither
/// captured Kinovi request carries a `generate_audio` key.
async fn assert_endpoints_omit_generate_audio(model: CommonVideoModel) {
let harness = TestHarness::create().await;
// omni_gen, session-cookie user.
let web_user = harness.create_funded_user(STARTING_CREDITS).await;
let mut request = base_generate_request(model);
let web_prompt = format!("audio flag test web {}", request.idempotency_token.as_deref().unwrap());
request.prompt = Some(web_prompt.clone());
request.generate_audio = Some(false);
let response = harness
.post_generate(&web_user, request)
.await
.unwrap_or_else(|err| panic!("{:?}: omni_gen generation failed: {:?}", model, err));
assert!(response.success);
// omni_api, API-key user.
let api_user = harness.create_funded_user(STARTING_CREDITS).await;
let api_key = harness.create_api_key(&api_user).await;
let mut request = base_generate_request(model);
let api_prompt = format!("audio flag test api {}", request.idempotency_token.as_deref().unwrap());
request.prompt = Some(api_prompt.clone());
request.generate_audio = Some(false);
let response = harness
.post_generate_via_api_key(&api_key, request)
.await
.unwrap_or_else(|err| panic!("{:?}: omni_api generation failed: {:?}", model, err));
assert!(response.success);
let traffic = stub_traffic().lock().unwrap();
for (endpoint, prompt) in [("omni_gen", &web_prompt), ("omni_api", &api_prompt)] {
let params = traffic.workflows.iter()
.map(|request| &request["0"]["json"]["apiParams"])
.find(|params| params["prompt"].as_str() == Some(prompt))
.unwrap_or_else(|| panic!("{:?}: no captured Kinovi request for {}", model, endpoint));
assert!(
params.get("generate_audio").is_none(),
"{:?}: {} forwarded generate_audio to Kinovi: {}", model, endpoint, params,
);
}
}
@@ -74,7 +74,8 @@ async fn quicktime_uploads_are_video_references_with_mp4_or_mov_output() {
.find(|params| params["prompt"].as_str() == Some(&prompt))
.expect("captured Kinovi request");
assert_eq!(params["output_format"], expected);
assert_eq!(params["generate_audio"], true);
// Seedance 2.x never forwards the audio flag; Kinovi defaults to sound on.
assert!(params.get("generate_audio").is_none(), "{}", params);
assert_eq!(params["mode"], "reference");
assert!(params["videoUrls"][0].as_str().is_some_and(|url| url.ends_with(".mov")), "{}", params);
assert_eq!(traffic.uploads.get("fixture.mov").unwrap(), &bytes);
@@ -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)),
@@ -4,16 +4,15 @@
//!
//! Rate cards (ceil-rounded once):
//! - Seedance 2.5: 480p 11.76954733 ¢/s, 720p 26.70781893 ¢/s,
//! 1080p 45.85869386 ¢/s; with video refs 480p 7.24279835 ¢/s,
//! 720p 15.84362140 ¢/s, 1080p 27.39973680 ¢/s over (output + input)
//! 1080p 61.62732357 ¢/s; with video refs 480p 7.24279835 ¢/s,
//! 720p 15.84362140 ¢/s, 1080p 38.49728574 ¢/s over (output + input)
//! seconds
//! - Seedance 2.5 Ultra: 480p 13.90946502 ¢/s, 720p 31.56378601 ¢/s,
//! 1080p 50.10486922 ¢/s; with video refs 480p 8.55967078 ¢/s,
//! 720p 18.72427984 ¢/s, 1080p 29.93674947 ¢/s
//! 1080p 67.22980753 ¢/s; with video refs 480p 8.55967078 ¢/s,
//! 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;
@@ -90,60 +89,60 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(748)),
(Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(775)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(184)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(276)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(322)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(367)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(413)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(459)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(505)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(551)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(597)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(643)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(688)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(734)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(780)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(826)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(872)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(918)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(964)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1009)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1055)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1101)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1147)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1193)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1239)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1285)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1330)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1376)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(184)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(276)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(322)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(367)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(413)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(459)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(505)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(551)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(597)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(643)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(688)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(734)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(780)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(826)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(872)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(918)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(964)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1009)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1055)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1101)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1147)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1193)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1239)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1285)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1330)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1376)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(247)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(370)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(432)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(494)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(555)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(617)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(678)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(740)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(863)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(925)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(987)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1110)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1171)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1233)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1295)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1356)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1418)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1541)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1603)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1664)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1726)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1788)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1849)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(247)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(370)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(432)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(494)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(555)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(617)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(678)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(740)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(863)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(925)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(987)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1110)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1171)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1233)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1295)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1356)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1418)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1541)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1603)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1664)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1726)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1788)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1849)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -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::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(230)),
(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(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(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(276)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(322)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(413)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(505)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(597)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(688)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(276)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(322)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(413)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(505)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(597)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(688)),
(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 {
@@ -313,60 +276,60 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(919)),
(Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(935)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(951)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(932)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(987)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(1014)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(1042)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(1069)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(1096)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(1124)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(1151)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(1179)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(1206)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1233)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1261)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1288)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1316)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1343)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1370)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1398)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1425)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1453)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1507)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1535)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1562)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1590)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1644)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(932)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(987)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(1014)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(1042)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(1069)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(1096)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(1124)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(1151)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(1179)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(1206)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1233)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1261)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1288)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1316)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1343)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1370)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1398)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1425)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1453)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1507)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1535)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1562)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1590)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1644)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(1309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(1386)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(1425)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(1463)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(1502)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(1540)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(1579)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(1656)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(1694)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1733)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1771)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1810)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1848)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1887)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1925)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1964)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(2002)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(2041)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(2079)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(2118)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(2156)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(2195)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(2233)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(2272)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(2310)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(1309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(1386)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(1425)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(1463)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(1502)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(1540)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(1579)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(1656)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(1694)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1733)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1771)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1810)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1848)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1887)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1925)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1964)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(2002)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(2041)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(2079)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(2118)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(2156)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(2195)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(2233)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(2272)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(2310)),
];
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::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(959)),
(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(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(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(987)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1014)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1069)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(1124)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1179)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1233)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(987)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1014)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(1069)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(1124)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1179)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1233)),
(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 {
@@ -546,60 +473,60 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(884)),
(Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(916)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(947)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(201)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(301)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(351)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(401)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(451)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(502)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(552)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(602)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(652)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(702)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(752)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(852)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(902)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(952)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1003)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1053)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1103)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1153)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1203)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1253)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1303)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1353)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1403)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1454)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1504)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(201)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(301)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(351)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(401)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(451)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(502)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(552)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(602)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(652)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(702)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(752)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(852)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(902)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(952)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1003)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1053)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1103)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1153)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1203)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1253)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1303)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1353)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1403)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1454)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1504)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(269)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(404)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(471)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(538)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(606)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(673)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(740)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(807)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(874)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(942)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1009)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1076)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1143)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1211)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1278)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1345)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1412)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1547)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1614)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1681)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1748)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1816)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1883)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1950)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(2017)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(269)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(404)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(471)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(538)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(606)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(673)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(740)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(807)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(874)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(942)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1009)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1076)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1143)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1211)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1278)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1345)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1412)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1547)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1614)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1681)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1748)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1816)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1883)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1950)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(2017)),
];
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::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(251)),
(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(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(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(301)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(351)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(451)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(552)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(652)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(752)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(301)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(351)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(451)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(552)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(652)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(752)),
(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 {
@@ -769,60 +660,60 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(1087)),
(Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(1105)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(1124)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(1018)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(1078)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(1108)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(1138)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(1168)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(1198)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(1228)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(1258)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(1288)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(1318)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1378)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1408)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1437)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1467)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1497)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1527)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1557)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1587)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1647)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1677)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1707)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1737)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1767)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1797)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(1018)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(1078)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(1108)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(1138)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(1168)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(1198)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(1228)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(1258)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(1288)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(1318)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1378)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1408)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1437)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1467)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1497)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1527)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1557)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1587)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1647)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1677)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1707)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1737)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1767)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1797)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(1423)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(1507)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(1549)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(1591)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(1632)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(1674)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(1716)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(1758)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(1800)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(1842)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1884)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1925)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1967)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(2009)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(2051)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(2093)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(2135)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(2176)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(2218)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(2260)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(2302)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(2344)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(2386)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(2428)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(2469)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(2511)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(1423)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(1507)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(1549)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(1591)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(1632)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(1674)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(1716)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(1758)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(1800)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(1842)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1884)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1925)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1967)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(2009)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(2051)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(2093)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(2135)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(2176)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(2218)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(2260)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(2302)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(2344)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(2386)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(2428)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(2469)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(2511)),
];
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::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(1048)),
(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(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(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(1078)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1108)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1168)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(1228)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1288)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1078)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1108)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(1168)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(1228)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1288)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1348)),
(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 {
@@ -1059,7 +914,7 @@ mod input_video_duration_billing {
// 6.5s → 7 → 37 billed × 7.24279835 = 267.98 → 268
// 10.5s → 11 → 41 billed × 7.24279835 = 296.95 → 297
// 6.5s → 7 → 37 billed × 15.84362140 = 586.21 → 587 (720p)
// 6.5s → 7 → 37 billed × 27.39973680 = 1013.79 → 1014 (1080p)
// 6.5s → 7 → 37 billed × 38.49728574 = 1424.40 → 1425 (1080p)
assert_real_input_videos_charge(
&harness, CommonVideoModel::Seedance2p5, Some(CommonResolution::FourEightyP),
Seconds(30), &[4_500], ExpectedCredits(254),
@@ -1078,7 +933,7 @@ mod input_video_duration_billing {
).await;
assert_real_input_videos_charge(
&harness, CommonVideoModel::Seedance2p5, Some(CommonResolution::TenEightyP),
Seconds(30), &[6_500], ExpectedCredits(1014),
Seconds(30), &[6_500], ExpectedCredits(1425),
).await;
}
@@ -1122,12 +977,12 @@ mod input_video_duration_billing {
let harness = TestHarness::create().await;
// 30s output, ultra with-refs 480p 8.55967078 ¢/s / 720p 18.72427984 ¢/s
// / 1080p 29.93674947 ¢/s:
// / 1080p 41.84487580 ¢/s:
// 2.5s → total 3 → clamps to 4 → 34 billed → 291.03 → 292
// 10.5s → 11 → 41 billed → 350.95 → 351
// 2.5s + 6.5s → 3+7=10 → 40 billed → 342.39 → 343 (total-based, no per-video clamp)
// 6.5s → 7 → 37 billed × 18.72427984 = 692.80 → 693 (720p)
// 6.5s → 7 → 37 billed × 29.93674947 = 1107.66 → 1108 (1080p)
// 6.5s → 7 → 37 billed × 41.84487580 = 1548.26 → 1549 (1080p)
assert_real_input_videos_charge(
&harness, CommonVideoModel::Seedance2p5Ultra, Some(CommonResolution::FourEightyP),
Seconds(30), &[2_500], ExpectedCredits(292),
@@ -1146,7 +1001,7 @@ mod input_video_duration_billing {
).await;
assert_real_input_videos_charge(
&harness, CommonVideoModel::Seedance2p5Ultra, Some(CommonResolution::TenEightyP),
Seconds(30), &[6_500], ExpectedCredits(1108),
Seconds(30), &[6_500], ExpectedCredits(1549),
).await;
}
}
@@ -1172,10 +1027,10 @@ mod premium {
(Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(268), ExpectedCredits(316), CreditsDelta(48)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(401), ExpectedCredits(474), CreditsDelta(73)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(802), ExpectedCredits(947), CreditsDelta(145)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(230), ExpectedCredits(251), CreditsDelta(21)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1376), ExpectedCredits(1504), CreditsDelta(128)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(309), ExpectedCredits(337), CreditsDelta(28)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1849), ExpectedCredits(2017), CreditsDelta(168)),
// 4K downgrades to 1080p on both models, so the premium matches 1080p.
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(230), ExpectedCredits(251), CreditsDelta(21)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(309), ExpectedCredits(337), CreditsDelta(28)),
];
for (resolution, seconds, batch, base, variant, delta) 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(())
@@ -0,0 +1,9 @@
export const AUTH_LABEL_CLASSES =
"block font-mono text-[11px] font-semibold leading-4 uppercase tracking-[0.12em] text-white/70";
export const AUTH_INPUT_CLASSES =
"h-10 w-full rounded-[3px] border border-white/15 bg-white/5 px-4 py-2 text-sm font-normal leading-5 text-white placeholder-white/20 outline-none transition-colors focus:border-white/40";
export const AUTH_PASSWORD_INPUT_CLASSES = `${AUTH_INPUT_CLASSES} pr-12`;
export const AUTH_FORM_PADDING = "px-6 py-10 sm:px-10 sm:py-12";
@@ -1,11 +1,11 @@
import { AUTH_FORM_PADDING } from "./auth-form-styles";
import { ReactNode } from "react";
import { Outlet, useNavigate, Navigate } from "react-router-dom";
import { ArrowLeftIcon, LoaderCircleIcon } from "lucide-react";
import { TruchetPattern } from "@storyteller/ui-vfx";
import { Outlet, Navigate, Link } from "react-router-dom";
import { LoaderCircleIcon } from "lucide-react";
import { AuthShowcase } from "./auth-showcase";
import { AuthPageFrame } from "./auth-page-frame";
import { useMediaQuery } from "../ui/use-media-query";
import { useSession } from "../../lib/session";
import { Reveal, RevealGroup } from "../motion/reveal";
/**
* Persistent shell for the auth pages. Rendered as a layout route so the
@@ -18,52 +18,21 @@ export const AuthLayout = () => {
// Only mount the showcase on wide screens (matches the `lg` breakpoint) so
// mobile never downloads the demo videos.
const showShowcase = useMediaQuery("(min-width: 1024px)");
const navigate = useNavigate();
const { loggedIn, authChecked } = useSession();
const handleBack = () => {
navigate("/");
};
// Already signed in? Never show the auth form — go straight home.
if (loggedIn) {
return <Navigate to="/" replace />;
}
return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-ui-background p-4 text-white">
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-0"
style={{
maskImage:
"radial-gradient(ellipse 70% 60% at 50% 40%, black 25%, transparent 80%)",
WebkitMaskImage:
"radial-gradient(ellipse 70% 60% at 50% 40%, black 25%, transparent 80%)",
}}
>
<TruchetPattern
intensity={0.5}
className="absolute inset-0 h-full w-full"
/>
</div>
<div className="relative z-10 flex w-full max-w-5xl overflow-hidden border border-white/15 bg-[#101014] lg:min-h-[640px]">
<AuthPageFrame wide>
{/* Form pane — only the inner content swaps per route (via Outlet), so
this pane and the showcase beside it stay mounted across the
login/signup toggle. */}
<div className="relative flex w-full flex-col lg:w-1/2">
<button
type="button"
onClick={handleBack}
aria-label="Go back"
className="absolute left-5 top-5 z-20 flex h-9 w-9 items-center justify-center text-white/60 transition-colors hover:bg-white/10 hover:text-white"
>
<ArrowLeftIcon />
</button>
<div className="flex flex-1 flex-col justify-center px-6 pb-10 pt-16 sm:px-10">
<div className="mx-auto w-full max-w-sm">
<div className={`flex flex-1 flex-col justify-center ${AUTH_FORM_PADDING}`}>
<div className="w-full">
{authChecked ? (
<Outlet />
) : (
@@ -76,44 +45,39 @@ export const AuthLayout = () => {
</div>
</div>
<div className="px-8 pb-8 text-center text-xs text-white/60">
&copy; {new Date().getFullYear()} ArtCraft. All rights reserved.
</div>
</div>
{/* Showcase pane (desktop only) */}
{showShowcase && (
<div className="relative lg:w-1/2">
<div className="relative border-l border-white/15 lg:w-1/2">
<AuthShowcase />
</div>
)}
</div>
</div>
</AuthPageFrame>
);
};
interface AuthHeaderProps {
title: string;
title: ReactNode;
subtitle: string;
}
export const AuthHeader = ({ title, subtitle }: AuthHeaderProps) => (
<RevealGroup inView={false} stagger={0.08} className="mb-8 text-center">
<Reveal y={10}>
<img
src="/artcraft-icon.svg"
alt="ArtCraft"
className="mx-auto mb-6 h-12 w-auto select-none pointer-events-none"
draggable={false}
/>
</Reveal>
<Reveal as="h1" y={10} className="mb-2 text-2xl font-semibold">
<div className="mb-8 text-left">
<Link
to="/"
aria-label="ArtCraft home"
className="mb-8 inline-flex rounded-sm transition-opacity hover:opacity-80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white/60"
>
<img src="/artcraft-icon.svg" alt="ArtCraft" className="h-8 w-8" />
</Link>
<h1 className="mb-3 text-balance font-display text-3xl leading-[1.05] tracking-tight sm:text-4xl">
{title}
</Reveal>
<Reveal as="p" y={10} className="text-sm text-white/60">
</h1>
<p className="text-sm leading-relaxed text-white/60">
{subtitle}
</Reveal>
</RevealGroup>
</p>
</div>
);
export const AuthFooter = ({ children }: { children: ReactNode }) => (
@@ -0,0 +1,22 @@
import { type ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import { FrameCornerMarks } from "../ui/frame-corner-marks";
/** A contained section frame shared by the auth pages. */
export function AuthPageFrame({ children, wide = false }: {
children: ReactNode;
wide?: boolean;
}) {
return (
<div className="relative isolate flex min-h-dvh items-center justify-center overflow-hidden bg-ui-background p-4 text-white sm:py-10">
<div className={twMerge("relative w-full", wide ? "max-w-lg lg:max-w-5xl" : "max-w-lg")}>
<div className="relative border border-white/20 bg-ui-controls">
<div className={twMerge("flex", wide ? "lg:min-h-[560px]" : "flex-col")}>
{children}
</div>
</div>
<FrameCornerMarks />
</div>
</div>
);
}
@@ -3,7 +3,7 @@ const FEATURED_VIDEO_URL =
export const AuthShowcase = () => {
return (
<div className="absolute inset-2 overflow-hidden bg-black">
<div className="absolute inset-0 overflow-hidden bg-ui-controls">
{/* Muted background video (no controls) cropped to cover the pane */}
<video
src={FEATURED_VIDEO_URL}
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_INPUT_CLASSES, AUTH_PASSWORD_INPUT_CLASSES } from "./auth-form-styles";
import { EyeIcon, EyeOffIcon, LoaderCircleIcon } from "lucide-react";
import { DynamicIcon } from "@storyteller/icons";
import { Button } from "@storyteller/ui-button";
@@ -14,7 +15,6 @@ import {
import { refreshSession } from "../../lib/session";
import { hasActiveSubscription } from "../../lib/billing";
import { GoogleLoginButton } from "./GoogleLoginButton";
import { Reveal, RevealGroup } from "../motion/reveal";
interface SignupFormProps {
onSuccess: (isNewUser?: boolean) => void;
@@ -98,7 +98,7 @@ export const SignupForm = ({
};
return (
<div className={`space-y-4 ${className}`}>
<div className={className}>
<form
className="space-y-4"
onSubmit={(e) => {
@@ -111,19 +111,11 @@ export const SignupForm = ({
{error}
</div>
)}
{/* Fields cascade in as their own beat, leading with the email field on
an almost-zero delay so the primary input lands immediately — the
entrance never gates typing (inputs stay focusable mid-cascade).
Mirrors the login page. */}
<RevealGroup
inView={false}
delayChildren={0.04}
stagger={0.08}
<div
className="space-y-4"
>
<Reveal className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70">
<div className="space-y-2">
<label className={AUTH_LABEL_CLASSES}>
Email
</label>
<Input
@@ -132,13 +124,13 @@ export const SignupForm = ({
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
autoFocus={autoFocus}
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 rounded-[3px] px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
inputClassName={AUTH_INPUT_CLASSES}
/>
</Reveal>
</div>
<Reveal className="space-y-2">
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70">
<label className={AUTH_LABEL_CLASSES}>
Password
</label>
</div>
@@ -148,7 +140,7 @@ export const SignupForm = ({
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 8 characters"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 rounded-[3px] px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -159,9 +151,9 @@ export const SignupForm = ({
<DynamicIcon icon={showPassword ? EyeOffIcon : EyeIcon} />
</button>
</div>
</Reveal>
</div>
<Reveal className="pt-2">
<div className="pt-2">
<Button
className="w-full justify-center h-10"
type="submit"
@@ -173,36 +165,25 @@ export const SignupForm = ({
"Create account"
)}
</Button>
</Reveal>
</RevealGroup>
</div>
</div>
</form>
{/* Secondary sign-up options continue the same cascade just after the
fields (delay continues from the form group's three children above:
0.04 + 3 × 0.08). */}
<RevealGroup
inView={false}
delayChildren={0.28}
stagger={0.08}
className="space-y-4"
>
<Reveal className="relative flex items-center justify-center py-2">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/10" />
</div>
<span className="relative bg-[#101014] px-4 font-mono text-[11px] uppercase tracking-[0.12em] text-white/40">
<div>
<div className="my-6 flex items-center gap-4 before:h-px before:flex-1 before:bg-white/15 after:h-px after:flex-1 after:bg-white/15">
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/40">
or
</span>
</Reveal>
</div>
<Reveal>
<div>
<GoogleLoginButton
mode="signup"
onSuccess={handleGoogleSuccess}
onError={handleGoogleError}
/>
</Reveal>
</RevealGroup>
</div>
</div>
</div>
);
};
@@ -0,0 +1,25 @@
import { type ReactNode } from "react";
import { FrameCornerMarks } from "../ui/frame-corner-marks";
interface CreateEmptyStateProps {
title: string;
subtitle: string;
children?: ReactNode;
compact?: boolean;
}
/** A quiet editorial section with a fine border and corner registration marks. */
export function CreateEmptyState({ title, subtitle, children, compact = false }: CreateEmptyStateProps) {
return (
<section className={`create-empty-section relative w-full max-w-3xl px-6 text-left sm:px-10 ${compact ? "py-8" : "py-10 sm:py-14"}`}>
<h1 className={`text-balance font-display leading-[1.05] tracking-[-0.035em] text-ui-ink ${compact ? "text-2xl" : "text-3xl md:text-5xl"}`}>
{title}
</h1>
<p className="mt-4 max-w-xl text-pretty text-sm leading-relaxed text-white/60 sm:text-base">
{subtitle}
</p>
{children && <div className="mt-6">{children}</div>}
<FrameCornerMarks />
</section>
);
}
@@ -1,3 +1,4 @@
import { CreateEmptyState } from "./CreateEmptyState";
import { type ReactNode, useState } from "react";
import { LoaderCircleIcon } from "lucide-react";
import { type PopoverItem } from "@storyteller/ui-popover";
@@ -127,14 +128,8 @@ export function CreateMediaPageShell({
<div className="px-3">{gridContent}</div>
</div>
) : (
<div className="relative isolate flex h-full flex-col items-center justify-center px-6 text-center">
<div aria-hidden="true" className="create-empty-backdrop" />
<span className="relative z-[1] font-display text-lg text-ui-ink">
{emptyStateTitle}
</span>
<span className="relative z-[1] pt-1 text-sm text-white/60">
Your generations will appear here.
</span>
<div className="flex h-full items-center justify-center overflow-y-auto px-6 py-8">
<CreateEmptyState compact title={emptyStateTitle} subtitle="Your generations will appear here." />
</div>
)}
</div>
@@ -149,25 +144,13 @@ export function CreateMediaPageShell({
<div className="relative isolate flex h-full w-full bg-ui-background text-white">
<Seo title={title} description={description} />
{/* Static landing-style rails stay inside the workspace and never
intercept input or cover the gallery once results arrive. */}
{!hasContent && (
<div aria-hidden="true" className="create-empty-backdrop" />
)}
<div className="relative z-[1] h-full w-full">
<div className="flex h-full w-full flex-col">
{!hasContent && (
<div className="flex flex-1 items-center justify-center">
<div className="animate-fade-in-up relative z-20 mb-32 flex max-w-3xl flex-col items-center justify-center px-6 text-center">
<h1 className="font-display text-3xl leading-tight text-ui-ink md:text-5xl">
{emptyStateTitle}
</h1>
<span className="max-w-xl pt-3 text-sm leading-relaxed text-base-fg/70 md:text-base">
{emptyStateSubtitle}
</span>
{emptyStateCta && <div className="pt-6">{emptyStateCta}</div>}
</div>
<div className="flex flex-1 items-center justify-center px-6" style={{ paddingBottom: bottomOffset }}>
<CreateEmptyState title={emptyStateTitle} subtitle={emptyStateSubtitle}>
{emptyStateCta}
</CreateEmptyState>
</div>
)}
@@ -1,5 +1,6 @@
import { type ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import { NEUTRAL_BUTTON_HOVER_CLASSES } from "@storyteller/ui-button";
import { ChevronRightIcon } from "lucide-react";
interface MobileFieldButtonProps {
@@ -27,7 +28,8 @@ export function MobileFieldButton({
disabled={disabled}
className={twMerge(
"flex w-full items-center gap-3 border border-ui-panel-border bg-ui-controls px-3.5 py-3 text-left transition-colors",
"hover:bg-ui-controls/80 active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-50",
NEUTRAL_BUTTON_HOVER_CLASSES,
"disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-ui-controls disabled:hover:border-ui-panel-border",
className,
)}
>
@@ -0,0 +1,8 @@
const CORNERS = ["top-0 left-0", "top-0 right-0", "bottom-0 left-0", "bottom-0 right-0"];
/** Decorative registration marks for a positioned panel. */
export function FrameCornerMarks() {
return CORNERS.map((corner) => (
<span key={corner} aria-hidden="true" className={`frame-corner-mark ${corner}`} />
));
}
@@ -1,5 +1,6 @@
import { CreateEmptyState } from "../../components/generation-gallery/CreateEmptyState";
import { useCallback, useEffect, useRef, useState } from "react";
import { LoaderCircleIcon, SparklesIcon } from "lucide-react";
import { LoaderCircleIcon } from "lucide-react";
import {
PromptBoxVFX,
VFXResultCard,
@@ -267,7 +268,7 @@ export default function CreateVFX() {
className="relative z-10 flex h-full items-center justify-center px-3 sm:px-6"
style={{ paddingBottom: Math.max(promptBoxHeight + 32, 240) }}
>
<EmptyState
<CreateEmptyState
title="No background changes yet"
subtitle="Upload a source video and a reference image, then optionally add a prompt."
/>
@@ -330,18 +331,3 @@ export default function CreateVFX() {
</div>
);
}
interface EmptyStateProps {
title: string;
subtitle: string;
}
const EmptyState = ({ title, subtitle }: EmptyStateProps) => (
<div className="flex max-w-md flex-col items-center gap-4 text-center">
<div className="flex h-12 w-12 items-center justify-center border border-white/15 bg-white/5">
<SparklesIcon className="text-2xl text-white/40" />
</div>
<h3 className="text-2xl font-bold text-white">{title}</h3>
<p className="text-sm text-white/60 max-w-xs">{subtitle}</p>
</div>
);
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_INPUT_CLASSES, AUTH_FORM_PADDING } from "../../components/auth/auth-form-styles";
import { ArrowLeftIcon, LoaderCircleIcon, MailIcon } from "lucide-react";
import { Button } from "@storyteller/ui-button";
import { Input } from "@storyteller/ui-input";
@@ -5,6 +6,8 @@ import { useState } from "react";
import { Link } from "react-router-dom";
import { PasswordResetApi } from "@storyteller/api";
import { AuthHeader } from "../../components/auth/auth-layout";
import { AuthPageFrame } from "../../components/auth/auth-page-frame";
import Seo from "../../components/seo";
const ForgotPassword = () => {
@@ -41,28 +44,17 @@ const ForgotPassword = () => {
};
return (
<div className="relative min-h-screen bg-ui-background text-white overflow-hidden flex flex-col">
<AuthPageFrame>
<Seo
title="Reset Password - ArtCraft"
description="Reset your ArtCraft password."
/>
<main className="relative z-10 flex-1 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-[#101014] border border-white/15 p-6 py-8">
<main className={AUTH_FORM_PADDING}>
<div className="w-full">
{!submitted ? (
<>
<div className="text-center mb-8">
<img
src="/images/artcraft-icon.png"
alt="ArtCraft"
className="mx-auto mb-6 h-12 w-auto select-none pointer-events-none"
draggable={false}
/>
<h1 className="text-2xl font-semibold mb-2">Reset Password</h1>
<p className="text-white/60 text-sm">
Enter your email to receive reset instructions
</p>
</div>
<AuthHeader title={<>Reset your <span className="font-serif-italic">password.</span></>} subtitle="Enter your email or username to receive a reset code." />
<form
className="space-y-4"
@@ -78,7 +70,7 @@ const ForgotPassword = () => {
)}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Email or Username
</label>
<Input
@@ -87,7 +79,7 @@ const ForgotPassword = () => {
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
inputClassName={AUTH_INPUT_CLASSES}
/>
</div>
@@ -134,18 +126,14 @@ const ForgotPassword = () => {
<div className="mt-8 text-center text-sm">
<Link
to="/login"
className="text-white/40 hover:text-white transition-colors flex items-center justify-center gap-2"
className="text-white/60 hover:text-white transition-colors flex items-center justify-center gap-2"
>
<ArrowLeftIcon /> Back to Log in
</Link>
</div>
</div>
</main>
<div className="relative z-10 py-6 text-center text-white/20 text-xs">
&copy; {new Date().getFullYear()} ArtCraft. All rights reserved.
</div>
</div>
</AuthPageFrame>
);
};
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_INPUT_CLASSES, AUTH_PASSWORD_INPUT_CLASSES, AUTH_FORM_PADDING } from "../../components/auth/auth-form-styles";
import {
ArrowLeftIcon,
CircleCheckIcon,
@@ -12,6 +13,8 @@ import { useState } from "react";
import { Link, useNavigate, useSearchParams } from "react-router-dom";
import { PasswordResetApi, BillingApi } from "@storyteller/api";
import { AuthHeader } from "../../components/auth/auth-layout";
import { AuthPageFrame } from "../../components/auth/auth-page-frame";
import Seo from "../../components/seo";
const VerifyReset = () => {
@@ -102,30 +105,17 @@ const VerifyReset = () => {
};
return (
<div className="relative min-h-screen bg-ui-background text-white overflow-hidden flex flex-col">
<AuthPageFrame>
<Seo
title="Verify Password Reset - ArtCraft"
description="Enter your verification code and new password."
/>
<main className="relative z-10 flex-1 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-[#101014] border border-white/15 p-6 py-8">
<main className={AUTH_FORM_PADDING}>
<div className="w-full">
{!success ? (
<>
<div className="text-center mb-8">
<img
src="/images/artcraft-icon.png"
alt="ArtCraft"
className="mx-auto mb-6 h-12 w-auto select-none pointer-events-none"
draggable={false}
/>
<h1 className="text-2xl font-semibold mb-2">
Password Reset Verification
</h1>
<p className="text-white/60 text-sm">
Enter the code sent to your email address.
</p>
</div>
<AuthHeader title={<>Set a new <span className="font-serif-italic">password.</span></>} subtitle="Enter your reset code and choose a new password." />
<form
className="space-y-4"
@@ -142,7 +132,7 @@ const VerifyReset = () => {
{/* Verification Code */}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Verification Code
</label>
<Input
@@ -152,10 +142,10 @@ const VerifyReset = () => {
onChange={(e) => setVerificationCode(e.target.value)}
placeholder="Enter verification code"
isError={!!fieldErrors.verificationCode}
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
inputClassName={AUTH_INPUT_CLASSES}
/>
{fieldErrors.verificationCode && (
<p className="text-red-400 text-xs ml-1">
<p className="text-red-400 text-xs">
{fieldErrors.verificationCode}
</p>
)}
@@ -163,7 +153,7 @@ const VerifyReset = () => {
{/* New Password */}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
New Password
</label>
<div className="relative">
@@ -174,7 +164,7 @@ const VerifyReset = () => {
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Enter new password"
isError={!!fieldErrors.newPassword}
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -187,7 +177,7 @@ const VerifyReset = () => {
</button>
</div>
{fieldErrors.newPassword && (
<p className="text-red-400 text-xs ml-1">
<p className="text-red-400 text-xs">
{fieldErrors.newPassword}
</p>
)}
@@ -195,7 +185,7 @@ const VerifyReset = () => {
{/* Verify New Password */}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Verify New Password
</label>
<div className="relative">
@@ -206,7 +196,7 @@ const VerifyReset = () => {
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Enter new password again"
isError={!!fieldErrors.confirmPassword}
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -221,7 +211,7 @@ const VerifyReset = () => {
</button>
</div>
{fieldErrors.confirmPassword && (
<p className="text-red-400 text-xs ml-1">
<p className="text-red-400 text-xs">
{fieldErrors.confirmPassword}
</p>
)}
@@ -246,7 +236,7 @@ const VerifyReset = () => {
<div className="mt-8 text-center text-sm">
<Link
to="/forgot-password"
className="text-white/40 hover:text-white transition-colors flex items-center justify-center gap-2"
className="text-white/60 hover:text-white transition-colors flex items-center justify-center gap-2"
>
<ArrowLeftIcon /> Request a new code
</Link>
@@ -277,11 +267,7 @@ const VerifyReset = () => {
)}
</div>
</main>
<div className="relative z-10 py-6 text-center text-white/20 text-xs">
&copy; {new Date().getFullYear()} ArtCraft. All rights reserved.
</div>
</div>
</AuthPageFrame>
);
};
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_INPUT_CLASSES, AUTH_PASSWORD_INPUT_CLASSES } from "../../components/auth/auth-form-styles";
import { EyeIcon, EyeOffIcon, LoaderCircleIcon } from "lucide-react";
import { DynamicIcon } from "@storyteller/icons";
import { Button } from "@storyteller/ui-button";
@@ -11,7 +12,6 @@ import {
GoogleLoginButton,
} from "../../components/auth";
import Seo from "../../components/seo";
import { Reveal, RevealGroup } from "../../components/motion/reveal";
import { refreshSession } from "../../lib/session";
import { hasActiveSubscription } from "../../lib/billing";
@@ -70,7 +70,7 @@ const Login = () => {
title="Login - ArtCraft"
description="Login to your ArtCraft account."
/>
<AuthHeader title="Welcome Back" subtitle="Log in to your account" />
<AuthHeader title={<>Welcome <span className="font-serif-italic">back.</span></>} subtitle="Log in to your creative workspace." />
<form
className="space-y-4"
@@ -84,20 +84,11 @@ const Login = () => {
{error}
</div>
)}
{/* The form fields cascade in as their own beat, distinct from the
shell/header settling. We lead with the email field on an almost-zero
delay so the primary input lands immediately — the entrance never
gates typing: inputs stay focusable and accept keystrokes while the
password/button below are still cascading in. */}
<RevealGroup
inView={false}
delayChildren={0.04}
stagger={0.08}
<div
className="space-y-4"
>
<Reveal className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70 ml-1">
<div className="space-y-2">
<label className={AUTH_LABEL_CLASSES}>
Email or Username
</label>
<Input
@@ -105,12 +96,12 @@ const Login = () => {
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com or username"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
inputClassName={AUTH_INPUT_CLASSES}
/>
</Reveal>
<Reveal className="space-y-2">
<div className="flex justify-between items-center ml-1">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70">
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className={AUTH_LABEL_CLASSES}>
Password
</label>
<Link
@@ -126,7 +117,7 @@ const Login = () => {
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 8 characters"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -137,9 +128,9 @@ const Login = () => {
<DynamicIcon icon={showPassword ? EyeOffIcon : EyeIcon} />
</button>
</div>
</Reveal>
</div>
<Reveal className="pt-2">
<div className="pt-2">
<Button
className="w-full justify-center h-10"
type="submit"
@@ -151,32 +142,25 @@ const Login = () => {
"Log in"
)}
</Button>
</Reveal>
</RevealGroup>
</form>
{/* Secondary sign-in options pick up the same cascade just after the
fields (delay continues from the form group's three children above:
0.04 + 3 × 0.08). */}
<RevealGroup inView={false} delayChildren={0.28} stagger={0.08}>
<Reveal className="relative my-6 flex items-center justify-center">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/15" />
</div>
<span className="relative bg-[#101014] px-4 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/40">
</div>
</form>
<div>
<div className="my-6 flex items-center gap-4 before:h-px before:flex-1 before:bg-white/15 after:h-px after:flex-1 after:bg-white/15">
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/40">
or
</span>
</Reveal>
</div>
<Reveal>
<div>
<GoogleLoginButton
mode="login"
onSuccess={handleGoogleSuccess}
onError={handleGoogleError}
/>
</Reveal>
</div>
<Reveal>
<div>
<AuthFooter>
Don't have an account?{" "}
<Link
@@ -186,8 +170,8 @@ const Login = () => {
Sign up
</Link>
</AuthFooter>
</Reveal>
</RevealGroup>
</div>
</div>
</>
);
};
@@ -22,7 +22,7 @@ export function SceneSplashModal({
<Modal
isOpen={isOpen}
onClose={close}
className="w-full max-w-3xl overflow-hidden border border-white/15 bg-ui-modal p-0"
className="w-full max-w-3xl overflow-hidden border border-white/15 bg-ui-controls p-0"
childPadding={false}
backdropClassName="!bg-black/80"
closeOnOutsideClick
@@ -88,7 +88,7 @@ const Pricing = () => {
{hasPlan && (
<Button
variant="secondary"
className="mt-6 gap-2 rounded-none"
className="mt-6 gap-2 rounded-none text-[11px]"
onClick={() => setCreditsModalOpen(true)}
>
<CoinsIcon aria-hidden className="h-4 w-4" />
@@ -164,7 +164,7 @@ const Pricing = () => {
{isLoggedIn ? (
<Button
variant="secondary"
className="mt-6 gap-2 rounded-none"
className="mt-6 gap-2 rounded-none text-[11px]"
onClick={() => setCreditsModalOpen(true)}
>
<CoinsIcon aria-hidden className="h-4 w-4 text-primary" />
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_PASSWORD_INPUT_CLASSES, AUTH_FORM_PADDING } from "../../components/auth/auth-form-styles";
import { EyeIcon, EyeOffIcon, LoaderCircleIcon } from "lucide-react";
import { DynamicIcon } from "@storyteller/icons";
import { Button } from "@storyteller/ui-button";
@@ -6,6 +7,8 @@ import { useState } from "react";
import { useNavigate, Navigate } from "react-router-dom";
import { UsersApi } from "@storyteller/api";
import { useSession, refreshSession } from "../../lib/session";
import { AuthHeader } from "../../components/auth/auth-layout";
import { AuthPageFrame } from "../../components/auth/auth-page-frame";
import Seo from "../../components/seo";
import { toast } from "../../components/toast/toast";
@@ -66,26 +69,15 @@ const SetPassword = () => {
};
return (
<div className="relative min-h-screen bg-ui-background text-white overflow-hidden flex flex-col">
<AuthPageFrame>
<Seo
title="Set Password - ArtCraft"
description="Set a password for your ArtCraft account."
/>
<main className="relative z-10 flex-1 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-[#101014] border border-white/15 p-6 py-8">
<div className="text-center mb-8">
<img
src="/images/artcraft-icon.png"
alt="ArtCraft"
className="mx-auto mb-6 h-12 w-auto select-none pointer-events-none"
draggable={false}
/>
<h1 className="text-2xl font-semibold mb-2">Set a password</h1>
<p className="text-white/60 text-sm">
Create a password so you can also log into the desktop app
</p>
</div>
<main className={AUTH_FORM_PADDING}>
<div className="w-full">
<AuthHeader title={<>Set your <span className="font-serif-italic">password.</span></>} subtitle="Create a password to sign in on the web and desktop." />
<form
className="space-y-4"
@@ -101,7 +93,7 @@ const SetPassword = () => {
)}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Password
</label>
<div className="relative">
@@ -111,7 +103,7 @@ const SetPassword = () => {
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 8 characters"
autoFocus
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -125,7 +117,7 @@ const SetPassword = () => {
</div>
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Confirm password
</label>
<div className="relative">
@@ -134,7 +126,7 @@ const SetPassword = () => {
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Re-enter your password"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -165,11 +157,7 @@ const SetPassword = () => {
</form>
</div>
</main>
<div className="relative z-10 py-6 text-center text-white/20 text-xs">
&copy; {new Date().getFullYear()} ArtCraft. All rights reserved.
</div>
</div>
</AuthPageFrame>
);
};
@@ -1,7 +1,6 @@
import { Link, useNavigate } from "react-router-dom";
import { AuthHeader, AuthFooter, SignupForm } from "../../components/auth";
import Seo from "../../components/seo";
import { Reveal } from "../../components/motion/reveal";
const Signup = () => {
const navigate = useNavigate();
@@ -12,17 +11,13 @@ const Signup = () => {
title="Sign Up - ArtCraft"
description="Create your ArtCraft account."
/>
<AuthHeader title="Create an Account" subtitle="Join thousands of creators" />
{/* SignupForm runs its own field cascade internally (see signup-form.tsx).
The footer picks up the tail of that cascade — the form's six staggered
steps land by ~0.36s, so the footer follows at ~0.44s. */}
<AuthHeader title={<>Make it <span className="font-serif-italic">yours.</span></>} subtitle="Create your account and start crafting." />
<SignupForm
onSuccess={() => navigate("/welcome")}
signupSource="artcraft"
/>
<Reveal inView={false} delay={0.44}>
<div>
<AuthFooter>
Already have an account?{" "}
<Link
@@ -32,7 +27,7 @@ const Signup = () => {
Log in
</Link>
</AuthFooter>
</Reveal>
</div>
</>
);
};
+17 -29
View File
@@ -527,37 +527,25 @@
}
}
/* Quiet drafting grid and side rails echo the landing's section frames.
No masks, filters, animation, or viewport-sized layers on mobile. */
.create-empty-backdrop {
/* Fine registration marks echo the landing page section corners. */
.frame-corner-mark {
position: absolute;
inset: 0;
z-index: 0;
overflow: hidden;
pointer-events: none;
background-image:
linear-gradient(rgba(242, 241, 238, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(242, 241, 238, 0.025) 1px, transparent 1px);
background-size: 96px 96px;
background-position: center;
}
.create-empty-backdrop::before {
content: "";
position: absolute;
inset: 12% clamp(24px, 6%, 96px) 20%;
border: 1px solid rgba(242, 241, 238, 0.1);
}
.create-empty-backdrop::after {
content: "";
position: absolute;
top: 12%;
left: clamp(24px, 6%, 96px);
width: 11px;
height: 11px;
transform: translate(-5px, -5px);
margin: -5px;
pointer-events: none;
background:
linear-gradient(#74aaff, #74aaff) center / 1px 11px no-repeat,
linear-gradient(#74aaff, #74aaff) center / 11px 1px no-repeat;
linear-gradient(rgba(242, 241, 238, 0.4), rgba(242, 241, 238, 0.4)) center / 1px 11px no-repeat,
linear-gradient(rgba(242, 241, 238, 0.4), rgba(242, 241, 238, 0.4)) center / 11px 1px no-repeat;
}
/* Contained section borders match the landing without decorative extensions. */
.create-empty-section {
margin-inline: 12px;
border: 1px solid rgba(242, 241, 238, 0.2);
background: rgba(242, 241, 238, 0.015);
}
@media (min-width: 640px) {
.create-empty-section { margin-inline: 32px; }
}
@@ -101,7 +101,7 @@ export const ButtonDropdown = ({
<Menu.Items
static
className={twMerge(
"absolute z-20 mt-1 w-max divide-y divide-gray-100 overflow-hidden rounded-lg bg-brand-secondary py-1.5 shadow-xl focus:outline-none",
"absolute z-20 mt-2 w-max min-w-48 overflow-hidden rounded-[3px] border border-ui-panel-border bg-ui-controls p-1.5 text-base-fg focus:outline-none",
align === "left" ? "left-0" : "right-0",
)}
>
@@ -109,19 +109,19 @@ export const ButtonDropdown = ({
{options.map((option, index) => (
<Fragment key={index}>
{option.divider && (
<div className="my-1.5 border-t border-white/5" />
<div className="my-1.5 border-t border-white/15" />
)}
<Menu.Item>
{({ active }) => (
<button
disabled={option.disabled}
className={twMerge(
"duration-50 bg-brand-secondary font-medium text-white transition-all",
active ? "bg-brand-secondary-800" : "",
"rounded-[3px] bg-transparent font-medium text-base-fg transition-colors duration-150",
active ? "bg-white/10" : "",
option.disabled
? "pointer-events-none opacity-40"
: "",
"group flex w-full items-center py-1.5 pl-7 pr-4 text-sm",
"group flex w-full items-center gap-2 px-2.5 py-2 text-sm",
option.className,
)}
onClick={() => handleOptionClick(index)}
@@ -130,11 +130,11 @@ export const ButtonDropdown = ({
{option.icon && (
<DynamicIcon
icon={option.icon}
className="mr-2"
className="mr-2 h-4 w-4 shrink-0"
/>
)}
<div className="grow text-start">{option.label}</div>
<div className="ml-10 font-normal text-white/75">
<div className="ml-6 font-mono text-[11px] font-normal text-white/45">
{option.description && option.description}
</div>
{showSelected && (
@@ -15,12 +15,14 @@ interface ButtonIconSelectProps {
options: Option[];
onOptionChange?: (value: string) => void;
selectedOption?: string;
buttonClassName?: string;
}
export function ButtonIconSelect({
options,
onOptionChange,
selectedOption,
buttonClassName,
}: ButtonIconSelectProps) {
const [internalSelectedOption, setInternalSelectedOption] = useState<string>(
selectedOption || options[0].value
@@ -54,7 +56,8 @@ export function ButtonIconSelect({
text ? "h-auto w-auto gap-2 px-3 py-1.5" : "w-9",
internalSelectedOption === value
? "border-brand-primary bg-brand-primary/20"
: "border-transparent hover:bg-ui-panel/[0.4]"
: "border-transparent hover:bg-ui-panel/[0.4]",
buttonClassName,
)}
onClick={() => handleOptionChange(value)}
>
@@ -72,7 +75,8 @@ export function ButtonIconSelect({
text ? "h-auto w-auto gap-2 px-3 py-1.5" : "w-9",
internalSelectedOption === value
? "border-brand-primary bg-brand-primary/20"
: "border-transparent hover:bg-ui-panel/[0.4]"
: "border-transparent hover:bg-ui-panel/[0.4]",
buttonClassName,
)}
onClick={() => handleOptionChange(value)}
>
@@ -30,11 +30,11 @@ export const ToggleButton = ({
// PopoverMenu triggers). Flat control surface + hairline border,
// same idiom as the PopoverMenu triggers beside it — the old glassy
// backdrop-blur look predates the brutalist system.
"flex h-[34px] items-center justify-center rounded-[3px] border border-ui-controls-border bg-ui-controls text-sm text-white transition-colors",
hasLabel ? "px-3" : "w-[34px]",
"flex h-[34px] items-center justify-center rounded-[3px] border border-ui-controls-border bg-ui-controls text-white transition-colors",
hasLabel ? "px-3" : "w-[34px] p-0",
isActive
? "border-white/30 bg-brand-primary/40 hover:bg-brand-primary/40"
: "hover:border-white/30 hover:bg-ui-controls/80",
? "border-white/30 bg-brand-primary/40 hover:border-white/30 hover:bg-brand-primary/40"
: "hover:bg-ui-controls hover:bg-[linear-gradient(rgba(255,255,255,0.07),rgba(255,255,255,0.07))]",
className,
)}
variant="secondary"
@@ -48,7 +48,7 @@ export const ToggleButton = ({
/>
)}
{label && (
<span className="text-sm font-medium text-white/90">{label}</span>
<span className="whitespace-nowrap text-white/90">{label}</span>
)}
</span>
</Button>
@@ -4,6 +4,9 @@ import { DynamicIcon } from "@storyteller/icons";
import { twMerge } from "tailwind-merge";
import { ButtonHTMLAttributes, AnchorHTMLAttributes } from "react";
export const NEUTRAL_BUTTON_HOVER_CLASSES =
"hover:bg-white/[0.07] hover:border-white/25";
type AnchorProps = Omit<
AnchorHTMLAttributes<HTMLAnchorElement>,
keyof ButtonHTMLAttributes<HTMLButtonElement>
@@ -41,20 +44,20 @@ export const Button = ({
case "secondary": {
// Resting bg-white/5 tint (matching the home grid cards) so the
// button reads as a surface, not a bare outline on the page bg.
return "bg-white/5 text-base-fg border border-ui-controls-border hover:bg-white/10 hover:border-white/30";
return "bg-white/5 text-base-fg border border-ui-controls-border";
}
case "action": {
return "bg-ui-controls text-base-fg border border-ui-controls-border hover:bg-ui-controls/80";
return "bg-ui-controls text-base-fg border border-ui-controls-border";
}
case "destructive": {
return "bg-red-500 hover:bg-red-400 text-white";
}
case "ghost": {
return "bg-transparent text-base-fg/70 hover:bg-white/10 hover:text-base-fg";
return "bg-transparent text-base-fg/70 hover:text-base-fg";
}
case "primary":
default: {
return "bg-white hover:bg-white/80 text-black font-bold";
return "bg-white hover:bg-white/90 text-black font-bold";
}
}
}
@@ -66,6 +69,8 @@ export const Button = ({
const className = twMerge(
"w-fit rounded-[3px] font-mono text-xs font-semibold uppercase tracking-[0.12em] px-3.5 py-2 border border-transparent focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-white/60 transition-colors duration-150 flex gap-2 items-center justify-center",
getVariantClassNames(propsVariant),
(propsVariant === "secondary" || propsVariant === "action" || propsVariant === "ghost") &&
NEUTRAL_BUTTON_HOVER_CLASSES,
propsClassName,
disabledClass,
);
@@ -17,7 +17,7 @@ import {
getCreatorIcon,
getModelFamilyName,
} from "@storyteller/model-list";
import { ChevronUpIcon, CircleCheckIcon } from "lucide-react";
import { ChevronUpIcon, CheckIcon } from "lucide-react";
import { GenerationProvider } from "@storyteller/api-enums";
import { Tooltip } from "@storyteller/ui-tooltip";
import { defaultModelForPage } from "./defaultModelForPage";
@@ -118,8 +118,8 @@ function ProviderTooltipContent({
{getProviderDisplayName(p)}
</span>
{selectedProvider === p && (
<span className="text-primary text-xl font-bold bg-white rounded-full p-0 h-4 w-4 flex items-center justify-center">
<CircleCheckIcon />
<span className="ml-2 flex h-5 w-5 shrink-0 items-center justify-center text-white">
<CheckIcon aria-hidden="true" className="h-4 w-4" strokeWidth={2.5} />
</span>
)}
</button>
@@ -313,7 +313,7 @@ export function ClassyModelSelector({
mode="hoverSelect"
maxListHeight={maxListHeight}
{...popoverProps}
buttonClassName="rounded-[3px] bg-ui-controls/90 hover:bg-ui-controls text-left shadow-sm px-3 py-1 gap-3 border border-ui-controls-border"
buttonClassName="rounded-[3px] bg-ui-controls text-left shadow-sm px-3 py-1 gap-3 border border-ui-controls-border"
renderTrigger={(selectedItem) => {
const modelTitle =
selectedItem?.label ?? selectedModel?.selectorName ?? "";
@@ -43,7 +43,6 @@ import type { PopoverItem } from "@storyteller/ui-popover";
import { v4 as uuidv4 } from "uuid";
import { EngineContext } from "./contexts/EngineContext/EngineContext";
import { AnonHintChip } from "./comps/AnonHintChip";
import { AspectRatioMenu } from "./comps/AspectRatioMenu";
import { ControlPanelSceneObject } from "./comps/ControlPanelSceneObject";
import { Controls3D } from "./comps/Controls3D";
@@ -488,26 +487,25 @@ export const Stage3DBody = ({
className="absolute left-0 top-0 w-full"
onClick={handleOverlayClick}
>
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col items-start gap-2">
<div className="grid grid-cols-2 gap-x-4 gap-y-1 2xl:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]">
<div className="min-w-0 flex flex-col items-start gap-2">
{!isRecord && <ControlsTopButtons />}
{!isRecord && topBarStartSlot && (
<div className="pl-3">{topBarStartSlot}</div>
)}
</div>
<div className="flex flex-col items-center">
<div className="col-span-2 row-start-2 flex flex-col items-center 2xl:col-span-1 2xl:col-start-2 2xl:row-start-1">
<SceneModePill />
{!isRecord && (
<Controls3D showImageTo3DButton={showImageTo3DButton} />
)}
</div>
<div className="flex items-start justify-end gap-2 pr-3 pt-3">
<div className="col-start-2 row-start-1 flex min-w-0 flex-wrap items-start justify-end gap-2 pr-3 pt-3 2xl:col-start-3">
{/* Camera cluster: which camera drives the viewport +
the render-frame aspect ratio. */}
{!isRecord && <CameraStatusPill />}
{!isRecord && <AspectRatioMenu />}
{!isRecord && topBarEndSlot}
{!isRecord && <AnonHintChip />}
</div>
</div>
</div>
@@ -21,7 +21,7 @@ export const AnonHintChip = () => {
<button
type="button"
onClick={handleClick}
className="glass flex items-center gap-2 rounded-full border border-white/10 px-3 py-1.5 text-xs font-medium text-white/80 shadow-md transition-colors hover:bg-ui-controls/100 hover:text-white"
className="flex shrink-0 items-center gap-2 whitespace-nowrap rounded-[3px] border border-ui-panel-border bg-ui-controls px-3 py-1.5 text-xs font-medium text-white/80 transition-colors hover:bg-white/10 hover:text-white"
>
<LogInIcon className="opacity-70" />
Sign up to save
@@ -6,6 +6,7 @@ import { CameraAspectRatio } from "../../enums";
import { setCameraAspect } from "../../actions";
import { usePageSceneStore } from "../../PageSceneStore";
import { EngineContext } from "../../contexts/EngineContext";
import { TOOLBAR_BUTTON_CLASS_NAME } from "../toolbarStyles";
// Render-frame aspect ratio picker. Same popover pattern as the prompt
// boxes' AspectRatioPicker, but driving the 3D editor's camera letterbox
@@ -52,7 +53,7 @@ export const AspectRatioMenu = () => {
position="bottom"
panelTitle="Aspect Ratio"
showIconsInList
buttonClassName="glass glass-no-hover h-[34px] rounded-full px-3 text-xs font-medium shadow-xl"
buttonClassName={TOOLBAR_BUTTON_CLASS_NAME}
triggerIcon={<AspectRatioIcon ratio={current.proportions} />}
/>
</Tooltip>
@@ -209,7 +209,7 @@ export const AnimationsModal = () => {
<Modal
isOpen={animationsModalVisible}
onClose={handleClose}
className="relative h-[640px] max-w-4xl"
className="relative h-[640px] max-w-4xl bg-ui-controls"
childPadding={false}
showClose={false}
backdropClassName="bg-transparent"
@@ -228,8 +228,8 @@ export const AnimationsModal = () => {
</Modal.DragHandle>
<div className="grid h-full grid-cols-12 gap-3">
<div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:bg-white/10">
<div className="flex items-center justify-between gap-2.5 py-0.5">
<h2 className="text-[18px] font-semibold opacity-80">
<div className="flex h-10 items-center justify-between gap-2.5">
<h2 className="font-display text-lg tracking-tight text-white">
Animations
</h2>
</div>
@@ -240,9 +240,9 @@ export const AnimationsModal = () => {
key={tab.id}
variant={activeTab === tab.id ? "primary" : "secondary"}
className={twMerge(
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
"w-full justify-start rounded-[3px] border border-transparent bg-transparent px-3.5 py-2.5 text-left hover:bg-white/15",
activeTab === tab.id &&
"border-primary bg-primary/10 hover:bg-primary/10",
"border-white/15 bg-white/10 text-white hover:bg-white/10",
)}
onClick={() => setActiveTab(tab.id)}
>
@@ -259,7 +259,7 @@ export const AnimationsModal = () => {
variant="secondary"
icon={ArrowUpFromLineIcon}
iconClassName="opacity-70"
className="w-full justify-center rounded-xl border border-white/10 bg-white/[6%] px-3.5 py-2.5 hover:bg-white/15"
className="w-full justify-center rounded-[3px] border border-white/10 bg-white/[6%] px-3.5 py-2.5 hover:bg-white/15"
onClick={handleOpenUpload}
>
Upload animation
@@ -281,13 +281,13 @@ export const AnimationsModal = () => {
ref={searchInputRef}
placeholder="Search animations"
className="relative z-[51] grow"
inputClassName="pr-2.5"
inputClassName="bg-white/5 pr-10 placeholder-white/40"
icon={SearchIcon}
value={searchTerm}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setSearchTerm(e.target.value)
}
iconClassName="text-white/60"
iconClassName="pointer-events-none left-3 top-1/2 h-4 w-4 -translate-y-1/2 p-0 text-white/50"
/>
{searchTerm && (
<CloseButton
@@ -56,12 +56,12 @@ const AllTabSection = ({
<div className="mb-2 flex items-center justify-between">
<h3 className="text-md ml-2 font-semibold opacity-90">{label}</h3>
<Button
variant="secondary"
className="mr-3 flex items-center gap-1 px-2 py-1 text-xs"
variant="ghost"
className="mr-3 h-6 gap-1 px-1.5 py-0 text-[10px] tracking-[0.08em] leading-none"
onClick={onViewAll}
>
View all
<ChevronRightIcon className="text-xs opacity-70" />
<ChevronRightIcon className="h-3 w-3 shrink-0 opacity-70" />
</Button>
</div>
<div className="h-[170px]">
@@ -487,7 +487,7 @@ export const AssetModal = () => {
<Modal
isOpen={assetModalVisible}
onClose={handleClose}
className="relative h-[640px] max-w-4xl"
className="relative h-[640px] max-w-4xl bg-ui-controls"
childPadding={false}
showClose={false}
backdropClassName="bg-transparent"
@@ -507,8 +507,8 @@ export const AssetModal = () => {
</Modal.DragHandle>
<div className="grid h-full grid-cols-12 gap-3">
<div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:bg-white/10">
<div className="flex items-center justify-between gap-2.5 py-0.5">
<h2 className="text-[18px] font-semibold opacity-80">
<div className="flex h-10 items-center justify-between gap-2.5">
<h2 className="font-display text-lg tracking-tight text-white">
ArtCraft Presets
</h2>
</div>
@@ -519,9 +519,9 @@ export const AssetModal = () => {
key={tab.id}
variant={activeAssetTab === tab.id ? "primary" : "secondary"}
className={twMerge(
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
"w-full justify-start rounded-[3px] border border-transparent bg-transparent px-3.5 py-2.5 text-left hover:bg-white/15",
activeAssetTab === tab.id &&
"border-primary bg-primary/10 hover:bg-primary/10",
"border-white/15 bg-white/10 text-white hover:bg-white/10",
)}
onClick={() => setActiveAssetTab(tab.id)}
>
@@ -551,13 +551,13 @@ export const AssetModal = () => {
ref={searchInputRef}
placeholder="Search"
className="relative z-[51] grow"
inputClassName="pr-2.5"
inputClassName="bg-white/5 pr-10 placeholder-white/40"
icon={SearchIcon}
value={searchTerm}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setSearchTerm(e.target.value)
}
iconClassName="text-white/60"
iconClassName="pointer-events-none left-3 top-1/2 h-4 w-4 -translate-y-1/2 p-0 text-white/50"
/>
{searchTerm && (
<CloseButton
@@ -110,7 +110,7 @@ export const ItemElement = ({ item }: Props) => {
)}
<div
className="pointer-events-none relative aspect-[16/12] w-full select-none overflow-hidden rounded-xl border-[3px] border-white/5 bg-brand-secondary-600 object-cover object-center transition-all group-hover:border-brand-primary"
className="pointer-events-none relative aspect-[16/12] w-full select-none overflow-hidden rounded-none border border-white/15 bg-brand-secondary-600 object-cover object-center transition-all group-hover:border-brand-primary"
onPointerDown={(event) => dragAndDrop.onPointerDown(event, item, editor)}
onClick={handleClick}
style={{ cursor: "grab", pointerEvents: "auto" }}
@@ -1,11 +1,13 @@
import { useContext } from "react";
import { MoveIcon, VideoIcon } from "lucide-react";
import { DynamicIcon } from "@storyteller/icons";
import { Button } from "@storyteller/ui-button";
import { twMerge } from "tailwind-merge";
import { Tooltip } from "@storyteller/ui-tooltip";
import { EngineContext } from "../../contexts/EngineContext";
import { toggleCameraView } from "../../actions";
import { usePageSceneStore } from "../../PageSceneStore";
import { EditorStates } from "../../enums";
import { TOOLBAR_BUTTON_CLASS_NAME } from "../toolbarStyles";
// Small status pill (top-left) showing which camera the viewport is driving:
// the free "Viewport" camera vs looking through a render camera. Clicking it
@@ -28,21 +30,19 @@ export const CameraStatusPill = () => {
position="bottom"
delay={300}
>
<button
<Button
type="button"
variant="secondary"
icon={inCameraView ? VideoIcon : MoveIcon}
aria-pressed={inCameraView}
onClick={() => editor && toggleCameraView(editor)}
className={
inCameraView
? "flex h-[34px] items-center gap-2 rounded-full bg-brand-primary px-3 text-xs font-medium text-white shadow-xl transition-all duration-200"
: "glass glass-no-hover flex h-[34px] items-center gap-2 rounded-full px-3 text-xs font-medium text-base-fg/70 shadow-xl transition-all duration-200 hover:text-base-fg"
}
className={twMerge(
TOOLBAR_BUTTON_CLASS_NAME,
inCameraView && "border-white/30 bg-white/10",
)}
>
<DynamicIcon
icon={inCameraView ? VideoIcon : MoveIcon}
className="h-3 w-3"
/>
{inCameraView ? renderCameraLabel : "Viewport"}
</button>
</Button>
</Tooltip>
);
};
@@ -82,12 +82,12 @@ export const CompletionModal = () => {
return (
<div className="absolute inset-0 z-[70] flex items-center justify-center bg-black/60 p-6 backdrop-blur-sm">
<div className="glass glass-no-hover flex w-full max-w-xl flex-col gap-4 rounded-2xl p-5 text-white shadow-2xl">
<div className="border border-ui-panel-border bg-ui-controls flex w-full max-w-xl flex-col gap-4 rounded-none p-5 text-white">
<div className="text-sm font-semibold text-base-fg/90">
{isVideo ? "Recording complete" : "Capture complete"}
</div>
<div className="flex max-h-[52vh] items-center justify-center overflow-hidden rounded-xl bg-black/30">
<div className="flex max-h-[52vh] items-center justify-center overflow-hidden rounded-none bg-white/5">
{isVideo ? (
<video
src={artifact.objectUrl}
@@ -129,7 +129,7 @@ export const CompletionModal = () => {
type="button"
onClick={() => clearProducedArtifact()}
disabled={busy}
className="flex h-9 items-center gap-2 rounded-full px-3 text-sm text-base-fg/60 transition-colors hover:bg-white/10 hover:text-base-fg disabled:opacity-50"
className="flex h-9 items-center gap-2 rounded-[3px] px-3 text-sm text-base-fg/60 transition-colors hover:bg-white/10 hover:text-base-fg disabled:opacity-50"
>
<TrashIcon className="h-3.5 w-3.5" />
Discard
@@ -139,7 +139,7 @@ export const CompletionModal = () => {
<button
type="button"
onClick={() => void upload()}
className="flex h-9 items-center gap-2 rounded-full bg-brand-primary px-4 text-sm font-medium text-white transition-transform hover:scale-[1.03]"
className="flex h-9 items-center gap-2 rounded-[3px] bg-brand-primary px-4 text-sm font-medium text-white transition-colors hover:brightness-110"
>
<RotateCwIcon className="h-3.5 w-3.5" />
Retry
@@ -148,7 +148,7 @@ export const CompletionModal = () => {
<button
type="button"
onClick={() => void upload()}
className="flex h-9 items-center gap-2 rounded-full bg-brand-primary px-4 text-sm font-medium text-white transition-transform hover:scale-[1.03]"
className="flex h-9 items-center gap-2 rounded-[3px] bg-brand-primary px-4 text-sm font-medium text-white transition-colors hover:brightness-110"
>
<CloudUploadIcon className="h-3.5 w-3.5" />
Upload &amp; continue
@@ -305,7 +305,7 @@ export const ControlPanelSceneObject = () => {
as="div"
show={isShowing}
className={twMerge(
"glass absolute bottom-16 right-0 mb-4 mr-4 flex h-fit w-56 origin-bottom-right flex-col gap-2 rounded-lg border border-ui-panel-border p-3.5 text-white shadow-lg",
"border border-ui-panel-border bg-ui-controls absolute bottom-16 right-0 mb-4 mr-4 flex h-fit w-56 origin-bottom-right flex-col gap-2 rounded-none p-3.5 text-white",
)}
enter="transition-opacity duration-150"
enterFrom="opacity-0"
@@ -315,7 +315,7 @@ export const ControlPanelSceneObject = () => {
leaveTo="opacity-0"
style={{ transform: `scale(${getScale()})` }}
>
<div className="mb-1 flex justify-between">
<div className="mb-1 flex justify-between border-b border-ui-panel-border pb-3">
<div className="flex items-center gap-2">
<BoxIcon />
<p className="max-w-36 truncate font-semibold">
@@ -342,7 +342,7 @@ export const ControlPanelSceneObject = () => {
className={"flex flex-col gap-2 overflow-y-auto"}
>
<div className="flex flex-col gap-1">
<p className="font-semibold text-sm">Color</p>
<p className="font-mono text-[11px] font-medium uppercase tracking-[0.12em] text-white/60">Color</p>
{/* Native <input type="color"> is nested INSIDE the Button's
<label> so Safari opens the picker reliably. Safari refuses
to open a color picker triggered via label[htmlFor]→input
@@ -385,7 +385,7 @@ export const ControlPanelSceneObject = () => {
</Button>
</div>
<div className="flex flex-col gap-2">
<p className="font-semibold text-sm">Location</p>
<p className="font-mono text-[11px] font-medium uppercase tracking-[0.12em] text-white/60">Location</p>
<InputVector
x={localPosition.x.toString()}
y={localPosition.y.toString()}
@@ -398,7 +398,7 @@ export const ControlPanelSceneObject = () => {
</div>
<div className="flex flex-col gap-2">
<p className="font-semibold text-sm">Rotation</p>
<p className="font-mono text-[11px] font-medium uppercase tracking-[0.12em] text-white/60">Rotation</p>
<InputVector
x={localRotation.x.toString()}
y={localRotation.y.toString()}
@@ -413,7 +413,7 @@ export const ControlPanelSceneObject = () => {
<div className="mb-1 flex flex-col gap-2">
<DraggablePrecisionMutator onChange={handleUniformScaleChange}>
<p className="font-semibold text-sm">Scale</p>
<p className="font-mono text-[11px] font-medium uppercase tracking-[0.12em] text-white/60">Scale</p>
</DraggablePrecisionMutator>
<InputVector
x={localScale.x.toString()}
@@ -169,14 +169,14 @@ export const Controls3D = ({
return (
<>
<div className="flex justify-center pt-3">
<div className="glass rounded-2xl p-1.5 text-white shadow-xl h-fit">
<div className="border border-ui-panel-border bg-ui-controls rounded-none p-1.5 text-white h-fit">
<div className="flex items-center justify-center gap-1.5">
<div className="flex items-center gap-1.5">
<div className="relative">
{showEmptySceneTooltip && (
<div className="absolute -bottom-14 left-1/2 -translate-x-1/2 transform whitespace-nowrap">
<div className="animate-bounce rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white shadow-lg">
Click + to add your first 3D asset!
<div className="rounded-[3px] bg-primary px-4 py-2 text-sm font-medium text-white">
Click + to add your first 3D asset
<div className="absolute -top-1.5 left-1/2 h-3 w-3 -translate-x-1/2 rotate-45 transform bg-primary" />
</div>
</div>
@@ -236,7 +236,7 @@ export const Controls3D = ({
]}
onPanelAction={handleAddAssetAction}
showIconsInList
buttonClassName={`h-9 w-9 p-2 rounded-xl text-lg ${
buttonClassName={`h-9 w-9 p-2 rounded-[3px] text-lg ${
showEmptySceneTooltip
? "bg-primary/90 hover:bg-primary/70"
: "border-transparent bg-primary/90 hover:bg-primary/70"
@@ -254,7 +254,7 @@ export const Controls3D = ({
>
<Button
icon={WandSparklesIcon}
className="text-md h-9 w-9 rounded-[10px] bg-white/15 transition-colors hover:bg-white/25"
className="text-md h-9 w-9 rounded-[3px] bg-white/15 transition-colors hover:bg-white/25"
variant="secondary"
onClick={handleOpenCreate3dModal}
/>
@@ -262,13 +262,14 @@ export const Controls3D = ({
)}
</div>
<span className="opacity-10">|</span>
<span aria-hidden="true" className="h-5 w-px bg-white/15" />
<ButtonIconSelect
options={modes}
onOptionChange={handleModeChange}
selectedOption={selectedMode}
buttonClassName="rounded-[3px] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-300"
/>
<span className="opacity-10">|</span>
<span aria-hidden="true" className="h-5 w-px bg-white/15" />
{selectedMode === "scale" ? (
<Tooltip
content="Scale is always in local space"
@@ -277,7 +278,7 @@ export const Controls3D = ({
>
<button
disabled
className="flex min-w-[92px] cursor-not-allowed items-center justify-center gap-2 rounded-xl border border-white/10 bg-white/[0.04] px-3 py-1.5 text-sm font-medium text-base-fg opacity-40 transition-colors duration-150"
className="flex min-w-[92px] cursor-not-allowed items-center justify-center gap-2 rounded-[3px] border border-white/10 bg-white/[0.04] px-3 py-1.5 font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-base-fg opacity-40 transition-colors duration-150"
>
<BoxIcon className="h-3 w-3 text-base-fg/60" />
Local
@@ -290,7 +291,7 @@ export const Controls3D = ({
delay={300}
>
<button
className="flex min-w-[92px] items-center justify-center gap-2 rounded-xl border border-white/10 bg-white/[0.04] px-3 py-1.5 text-sm font-medium text-base-fg transition-colors duration-150 hover:bg-white/[0.08]"
className="flex min-w-[92px] items-center justify-center gap-2 rounded-[3px] border border-white/10 bg-white/[0.04] px-3 py-1.5 font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-base-fg transition-colors duration-150 hover:bg-white/[0.08]"
onClick={() => editor?.gizmo.toggleTransformSpace()}
>
<DynamicIcon
@@ -15,6 +15,8 @@ import { DEFAULT_CAMERA_ASPECT_RATIO, ToastTypes } from "../../enums";
import { getSceneGenerationMetaData } from "../../sceneMetadata";
import { LoadUserScenes } from "./LoadUserScenes";
import { TOOLBAR_BUTTON_CLASS_NAME } from "../toolbarStyles";
const isNumberString = (s: string): boolean => /^\d+$/.test(s);
export const ControlsTopButtons = () => {
@@ -170,7 +172,7 @@ export const ControlsTopButtons = () => {
// visitors). We deliberately do NOT require sceneMeta.isModified;
// dirty-tracking isn't plumbed end-to-end and saving shouldn't depend
// on it. Anon still falls through to the signup CTA branch below.
const canSave = !!currentUserToken;
const canSave = !!currentUserToken || !!editor?.adapter.promptSignup;
// A scene needs a name prompt before saving when it's brand-new (no
// token) or has no usable title yet. Already-named saved scenes save
@@ -239,11 +241,11 @@ export const ControlsTopButtons = () => {
return (
<div className="flex flex-col gap-2 pl-3 pt-3">
<div className="flex gap-1.5">
<div className="flex items-center gap-1.5">
<ButtonDropdown
label="File"
icon={FileIcon}
className="shadow-xl"
className={TOOLBAR_BUTTON_CLASS_NAME}
options={[
{
label: "New scene",
@@ -311,7 +313,9 @@ export const ControlsTopButtons = () => {
// that branch.
{
disabled: !canSave,
label: isVisitingOthersScene ? "Save copy" : "Save scene",
label: !currentUserToken
? "Sign up to save"
: isVisitingOthersScene ? "Save copy" : "Save scene",
description: "Ctrl+S",
...(!currentUserToken
? {
@@ -379,7 +383,7 @@ export const ControlsTopButtons = () => {
<Button
icon={outlinerShowing ? SquareCheckIcon : SquareIcon}
className="shadow-xl"
className={TOOLBAR_BUTTON_CLASS_NAME}
iconClassName={twMerge(
"text-[16px]",
outlinerShowing ? "text-white" : "text-white/20",
@@ -397,7 +401,7 @@ export const ControlsTopButtons = () => {
<Button
icon={KeyboardIcon}
variant="secondary"
className="shadow-xl"
className={TOOLBAR_BUTTON_CLASS_NAME}
iconClassName={twMerge(
"text-[16px]",
cheatsheetPinned ? "text-white" : "text-white/20",
@@ -25,7 +25,7 @@ export const ExitCameraView = ({ className }: { className?: string }) => {
type="button"
onClick={() => editor && toggleCameraView(editor)}
className={twMerge(
"flex h-11 items-center gap-2 whitespace-nowrap rounded-xl bg-brand-primary px-4 text-sm font-medium text-white shadow-xl transition-colors hover:bg-brand-primary/90",
"flex h-11 items-center gap-2 whitespace-nowrap rounded-[3px] bg-brand-primary px-4 text-sm font-medium text-white transition-colors hover:bg-brand-primary/90",
className,
)}
>
@@ -15,7 +15,7 @@ export const FocalLengthDisplay = () => {
leaveTo="opacity-0"
>
<div className="absolute left-1/2 top-16 z-10 -translate-x-1/2 transform">
<div className="glass rounded-xl px-5 py-2.5 text-center text-2xl font-bold text-white">
<div className="border border-ui-panel-border bg-ui-controls rounded-none px-5 py-2.5 text-center text-2xl font-bold text-white">
{focalLengthDragging.focalLength}mm
</div>
</div>
@@ -58,9 +58,9 @@ const OutlinerRow = ({ item }: { item: OutlinerItem }) => {
<div
role="button"
className={twMerge(
"flex cursor-pointer items-center justify-between gap-2 rounded-xl border border-transparent px-3 py-2 outline-none transition-colors duration-100 hover:bg-white/5 focus:outline-none",
"flex cursor-pointer items-center justify-between gap-2 rounded-[3px] border border-transparent px-3 py-2 transition-colors duration-100 hover:bg-white/5 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary-300",
isSelected &&
"border-brand-primary bg-brand-primary/15 hover:bg-brand-primary/15",
"border-white/20 bg-white/10 hover:bg-white/10",
)}
onDoubleClick={handleDoubleClick}
onClick={handleSelect}
@@ -146,7 +146,7 @@ export const Outliner = () => {
as="div"
show={showing}
className={twMerge(
"flex h-[45vh] w-[260px] flex-col overflow-hidden rounded-xl border border-ui-panel-border bg-ui-panel shadow-lg",
"flex h-[45vh] w-[260px] flex-col overflow-hidden rounded-[3px] border border-ui-panel-border bg-ui-controls",
)}
enter="transition-opacity duration-150"
enterFrom="opacity-0"
@@ -155,18 +155,23 @@ export const Outliner = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="flex items-center px-4 pb-1 pt-3">
<h1 className="grow text-base font-semibold">Scene</h1>
<div className="flex items-center border-b border-ui-panel-border px-3 py-2.5">
<h2 className="grow font-mono text-[11px] font-semibold uppercase tracking-[0.12em]">Scene</h2>
<Button
icon={PlusIcon}
className="h-6 bg-transparent px-1.5 text-sm font-medium text-white/80 hover:bg-transparent hover:text-white"
variant="ghost"
iconClassName="h-3.5 w-3.5 shrink-0"
className="h-7 gap-1 px-1.5 py-0 text-[11px] leading-none text-white/70 hover:text-white"
onClick={openAssetModal}
>
Add
</Button>
<Button
icon={XIcon}
className="h-5 bg-transparent p-0 text-xl opacity-50 hover:bg-transparent hover:opacity-90"
variant="ghost"
aria-label="Close scene panel"
iconClassName="h-4 w-4 shrink-0"
className="h-7 w-7 p-0 text-white/50 hover:text-white"
onClick={() => {
usePageSceneStore.getState().setOutlinerShowing(false);
}}
@@ -44,7 +44,7 @@ export const PoseModeSelector: React.FC<PoseModeSelectorProps> = () => {
<Button
icon={FootprintsIcon}
onClick={handleModeChange}
className="rounded-xl shadow-xl outline-none focus-visible:outline-none"
className="rounded-[3px] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-300"
>
Enter Pose Mode
</Button>
@@ -52,7 +52,7 @@ export const PoseModeSelector: React.FC<PoseModeSelectorProps> = () => {
<Button
icon={CheckIcon}
onClick={handleModeChange}
className="rounded-xl outline-none focus-visible:outline-none"
className="rounded-[3px] outline-none focus-visible:outline-none"
>
Done
</Button>
@@ -71,7 +71,7 @@ export const PoseModeSelector: React.FC<PoseModeSelectorProps> = () => {
<Button
icon={FilmIcon}
onClick={() => openAnimationsModal()}
className="rounded-xl shadow-xl outline-none focus-visible:outline-none"
className="rounded-[3px] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-300"
>
Add Animation
</Button>
@@ -27,7 +27,7 @@ export const PrecisionSelector = () => {
<span
onMouseEnter={() => handleMouseEnterItem(scale)}
key={index}
className="flex bg-ui-panel text-sm justify-center align-middle justify-items-center px-2 hover:bg-ui-controls"
className="flex bg-ui-controls text-sm justify-center align-middle justify-items-center px-2 hover:bg-white/10"
>
{scale}
</span>
@@ -84,7 +84,7 @@ export const PreviewEngineCamera = () => {
>
<div
className={twMerge(
"origin -z-10 flex h-auto w-full flex-wrap items-center gap-1.5 rounded-t-lg bg-ui-panel p-2 text-white",
"origin -z-10 flex h-auto w-full flex-wrap items-center gap-1.5 rounded-t-lg bg-ui-controls p-2 text-white",
camAspect !== CameraAspectRatio.VERTICAL_9_16
? "justify-between"
: "flex-col justify-center",
@@ -134,7 +134,7 @@ export const PreviewEngineCamera = () => {
: "aspect-video",
)}
>
<div className="flex h-full w-full items-center justify-center bg-ui-panel">
<div className="flex h-full w-full items-center justify-center bg-ui-controls">
<LoaderCircleIcon size="3em" className="animate-spin" />
</div>
<div className="absolute left-0 top-0 h-full w-full overflow-hidden">
@@ -13,7 +13,7 @@ export const PreviewImages = () => {
return (
<div className="flex justify-center gap-1">
<div
className="block w-full overflow-hidden rounded-lg border border-ui-controls/25 bg-ui-panel"
className="block w-full overflow-hidden rounded-lg border border-ui-controls/25 bg-ui-controls"
style={{ height: imageHeight, width: imageWidth }}
>
<canvas id="raw-preview" width={imageWidth} height={imageHeight} />
@@ -24,7 +24,7 @@ export const PreviewImages = () => {
className="text-3xl opacity-60" />
</div>
<div
className="block w-full overflow-hidden rounded-lg border border-ui-controls/25 bg-ui-panel"
className="block w-full overflow-hidden rounded-lg border border-ui-controls/25 bg-ui-controls"
style={{ height: imageHeight, width: imageWidth }}
>
<img
@@ -31,7 +31,7 @@ export const RecordControls = () => {
type="button"
onClick={() => editor && captureStill(editor)}
disabled={busy}
className="glass glass-no-hover flex items-center gap-2 rounded-full px-5 py-2.5 text-sm font-medium text-base-fg shadow-xl hover:bg-white/10 disabled:opacity-50"
className="border border-ui-panel-border bg-ui-controls flex items-center gap-2 rounded-[3px] px-5 py-2.5 text-sm font-medium text-base-fg hover:bg-white/10 disabled:opacity-50"
>
<CameraIcon className="h-3.5 w-3.5" />
Capture
@@ -49,7 +49,7 @@ export const RecordControls = () => {
type="button"
onClick={() => editor && void recordVideo(editor)}
disabled={busy || !timelineExists}
className="flex items-center gap-2 rounded-full bg-red px-5 py-2.5 text-sm font-semibold text-white shadow-xl transition-transform hover:scale-[1.03] disabled:opacity-50 disabled:hover:scale-100"
className="flex items-center gap-2 rounded-[3px] bg-red px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:brightness-110 disabled:opacity-50"
>
<CircleIcon className="h-3 w-3" />
Record
@@ -22,9 +22,9 @@ export const SceneModePill = () => {
activeTab={sceneMode}
onTabChange={(id) => setSceneMode(id as SceneMode)}
className="w-auto"
listClassName="rounded-full px-1"
tabClassName="rounded-full px-5 py-1"
indicatorClassName="rounded-full bg-white/95"
listClassName="rounded-none border border-ui-panel-border bg-ui-controls px-1"
tabClassName="rounded-[3px] px-5 py-1 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary-300"
indicatorClassName="rounded-[3px] bg-white/95"
selectedTabClassName="text-black"
/>
</div>
@@ -75,7 +75,7 @@ export const MotionPopover = ({
return (
<div
id="motion-popover"
className="glass glass-no-hover pointer-events-auto absolute bottom-full z-50 mb-2 w-[180px] -translate-x-1/2 select-none rounded-2xl p-3 shadow-xl backdrop-blur-md"
className="border border-ui-panel-border bg-ui-controls pointer-events-auto absolute bottom-full z-50 mb-2 w-[180px] -translate-x-1/2 select-none rounded-none p-3"
style={{ left: `${Math.max(12, Math.min(88, leftPercent))}%` }}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
@@ -95,7 +95,7 @@ export const MotionPopover = ({
type="button"
title={p.label}
onClick={() => commit({ ...preset })}
className={`flex h-8 items-center justify-center rounded-md border transition-colors ${
className={`flex h-8 items-center justify-center rounded-[3px] border transition-colors ${
active
? "border-brand-primary bg-brand-primary/20"
: "border-ui-controls-border/60 hover:bg-ui-controls/40"
@@ -118,7 +118,7 @@ export const MotionPopover = ({
<svg
ref={svgRef}
viewBox={`0 0 ${BOX} ${BOX}`}
className="w-full touch-none rounded-lg bg-black/20"
className="w-full touch-none rounded-[3px] bg-white/5"
onPointerMove={onPointerMove}
onPointerUp={() => setDragging(null)}
onPointerLeave={() => setDragging(null)}
@@ -25,22 +25,24 @@ export const TimelineBar = ({ readOnly = false }: { readOnly?: boolean }) => {
return (
<div
className="glass glass-no-hover flex w-full select-none items-center gap-3 rounded-2xl px-4 py-2 text-white shadow-xl"
className="border border-ui-panel-border bg-ui-controls flex w-full select-none items-center gap-3 rounded-none px-4 py-2 text-white"
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<button
type="button"
onClick={togglePlay}
className="flex h-7 w-7 items-center justify-center rounded-full text-base-fg/80 hover:bg-white/10"
aria-label={isPlaying ? "Pause timeline" : "Play timeline"}
className="flex h-7 w-7 items-center justify-center rounded-[3px] text-base-fg/80 hover:bg-white/10"
>
<DynamicIcon icon={isPlaying ? PauseIcon : PlayIcon} className="h-3.5 w-3.5" />
</button>
<span className="w-9 shrink-0 tabular-nums text-xs text-base-fg/70">
<span className="w-9 shrink-0 font-mono tabular-nums text-xs text-base-fg/70">
{formatTimecode(playhead)}
</span>
<input
type="range"
aria-label="Timeline playhead"
min={0}
max={duration}
step={0.01}
@@ -53,7 +55,7 @@ export const TimelineBar = ({ readOnly = false }: { readOnly?: boolean }) => {
className="h-1 flex-1 cursor-pointer accent-white"
/>
{readOnly ? (
<span className="w-9 shrink-0 text-right tabular-nums text-xs text-base-fg/70">
<span className="w-9 shrink-0 text-right font-mono tabular-nums text-xs text-base-fg/70">
{formatTimecode(duration)}
</span>
) : (
@@ -64,7 +66,7 @@ export const TimelineBar = ({ readOnly = false }: { readOnly?: boolean }) => {
type="button"
title="Expand timeline"
onClick={() => setExpanded(true)}
className="flex h-7 w-7 items-center justify-center rounded-full text-base-fg/60 hover:bg-white/10"
className="flex h-7 w-7 items-center justify-center rounded-[3px] text-base-fg/60 hover:bg-white/10"
>
<ChevronUpIcon className="h-3 w-3" />
</button>
@@ -232,7 +232,7 @@ export const TimelineClipRow = ({
/* overflow-hidden: strips are engine-clamped into the lane, but any
transient out-of-range placement must clip here rather than paint
over the add-button column. */
className="relative h-6 flex-1 overflow-hidden rounded-md bg-black/20"
className="relative h-6 flex-1 overflow-hidden rounded-md bg-white/5"
onPointerMove={onPointerMove}
onPointerUp={endDrag}
onPointerLeave={endDrag}
@@ -370,7 +370,7 @@ export const TimelineClipRow = ({
className={`absolute top-1/2 z-10 flex h-4 w-4 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border transition-colors ${
active
? "border-brand-primary bg-brand-primary text-white"
: "border-dashed border-white/30 bg-black/30 text-white/40 hover:border-white/60 hover:text-white/80"
: "border-dashed border-white/30 bg-white/5 text-white/40 hover:border-white/60 hover:text-white/80"
}`}
style={{ left: `${midPct}%` }}
onPointerDown={(e) => e.stopPropagation()}
@@ -317,24 +317,25 @@ export const TimelineEditor = () => {
return (
<div
id="timeline-editor"
className="glass glass-no-hover absolute bottom-4 left-1/2 w-[90vw] max-w-5xl -translate-x-1/2 select-none rounded-2xl p-3 text-white shadow-xl"
className="border border-ui-panel-border bg-ui-controls absolute bottom-4 left-1/2 w-[90vw] max-w-5xl -translate-x-1/2 select-none rounded-none p-3 text-white"
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
{/* transport + collapse */}
<div className="mb-2 flex items-center gap-2">
<div className="mb-2 flex items-center gap-2 border-b border-ui-panel-border pb-2">
<button
type="button"
title="Go to start"
onClick={() => editor && seekTimeline(editor, 0)}
className="flex h-7 w-7 items-center justify-center rounded-full text-base-fg/70 hover:bg-white/10"
className="flex h-7 w-7 items-center justify-center rounded-[3px] text-base-fg/70 hover:bg-white/10"
>
<SkipBackIcon className="h-3.5 w-3.5" />
</button>
<button
type="button"
onClick={togglePlay}
className="flex h-7 w-7 items-center justify-center rounded-full text-base-fg/90 hover:bg-white/10"
aria-label={isPlaying ? "Pause timeline" : "Play timeline"}
className="flex h-7 w-7 items-center justify-center rounded-[3px] text-base-fg/90 hover:bg-white/10"
>
<DynamicIcon
icon={isPlaying ? PauseIcon : PlayIcon}
@@ -345,11 +346,11 @@ export const TimelineEditor = () => {
type="button"
title="Go to end"
onClick={() => editor && seekTimeline(editor, duration)}
className="flex h-7 w-7 items-center justify-center rounded-full text-base-fg/70 hover:bg-white/10"
className="flex h-7 w-7 items-center justify-center rounded-[3px] text-base-fg/70 hover:bg-white/10"
>
<SkipForwardIcon className="h-3.5 w-3.5" />
</button>
<span className="ml-2 tabular-nums text-xs text-base-fg/60">
<span className="ml-2 font-mono tabular-nums text-xs text-base-fg/60">
{formatTimecodeFrames(playhead, fps)} / {formatTimecode(duration)}
</span>
{/* ml-auto lives on a wrapper: Tooltip's className styles the popup
@@ -368,7 +369,7 @@ export const TimelineEditor = () => {
<button
type="button"
onClick={() => setFocusSelected(!focusSelected)}
className={`flex h-7 w-7 items-center justify-center rounded-full transition-colors ${
className={`flex h-7 w-7 items-center justify-center rounded-[3px] transition-colors ${
focusSelected
? "bg-primary/20 text-white"
: "text-base-fg/60 hover:bg-white/10"
@@ -382,7 +383,7 @@ export const TimelineEditor = () => {
type="button"
title="Collapse timeline"
onClick={() => setExpanded(false)}
className="flex h-7 w-7 items-center justify-center rounded-full text-base-fg/60 hover:bg-white/10"
className="flex h-7 w-7 items-center justify-center rounded-[3px] text-base-fg/60 hover:bg-white/10"
>
<ChevronDownIcon className="h-3 w-3" />
</button>
@@ -552,7 +553,7 @@ export const TimelineEditor = () => {
footer={
<button
type="button"
className="mt-2 w-full rounded-md border border-ui-controls-border/60 px-2 py-1.5 text-[11px] text-base-fg/80 transition-colors hover:bg-ui-controls/40"
className="mt-2 w-full rounded-[3px] border border-ui-controls-border/60 px-2 py-1.5 text-[11px] text-base-fg/80 transition-colors hover:bg-ui-controls/40"
onClick={() => {
if (editor) {
setClipTransitionEasing(
@@ -90,7 +90,7 @@ export const TimelineTrackRow = ({
<div
ref={laneRef}
className="relative h-9 flex-1 rounded-lg bg-black/30"
className="relative h-9 flex-1 rounded-lg bg-white/5"
onPointerMove={(e) => {
if (!draggingId.current || !editor) return;
moveKeyframe(
@@ -129,7 +129,7 @@ export const TimelineTrackRow = ({
type="button"
title="Edit easing"
data-easing-chip
className={`absolute top-1/2 flex h-5 w-6 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-md border bg-black/60 ${
className={`absolute top-1/2 flex h-5 w-6 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-md border bg-ui-controls ${
active
? "border-brand-primary text-white"
: "border-white/40 text-white/80 hover:border-white/80 hover:text-white"
@@ -0,0 +1,2 @@
export const TOOLBAR_BUTTON_CLASS_NAME =
"h-9 rounded-[3px] border border-ui-panel-border bg-ui-controls px-3 py-0 font-mono text-xs font-semibold uppercase tracking-[0.12em] leading-none shadow-none [&_svg]:h-3.5 [&_svg]:w-3.5 [&_svg]:shrink-0";
@@ -14,13 +14,12 @@ import {
PopoverPanel,
} from "@headlessui/react";
import { twMerge } from "tailwind-merge";
import { Button } from "@storyteller/ui-button";
import { Button, NEUTRAL_BUTTON_HOVER_CLASSES } from "@storyteller/ui-button";
import {
CheckIcon,
ChevronDownIcon,
ChevronRightIcon,
ChevronUpIcon,
CircleCheckIcon,
InfoIcon,
} from "lucide-react";
import { Model, ModelInfo } from "@storyteller/model-list";
@@ -143,7 +142,7 @@ function PortalTooltip({
zIndex: 9999,
}}
className={twMerge(
"pointer-events-auto rounded-[3px] bg-ui-panel p-3 border border-ui-panel-border text-base-fg",
"pointer-events-auto rounded-[3px] bg-ui-controls p-3 border border-ui-panel-border text-base-fg",
className,
)}
>
@@ -260,7 +259,7 @@ function InfoHint({ content }: { content: ReactNode }) {
transform: "translate(-50%, -100%)",
zIndex: 10000,
}}
className="pointer-events-auto rounded-[3px] border border-ui-panel-border bg-ui-panel px-3 py-1.5 text-center text-xs leading-relaxed text-base-fg"
className="pointer-events-auto rounded-[3px] border border-ui-panel-border bg-ui-controls px-3 py-1.5 text-center text-xs leading-relaxed text-base-fg"
onClick={(e) => e.stopPropagation()}
onMouseEnter={() => {
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
@@ -357,8 +356,8 @@ function RichListRow({
}}
className={twMerge(
"group flex cursor-pointer items-center gap-3 rounded-[3px] px-2 py-2 transition-colors",
item.selected ? "bg-ui-controls/70" : "hover:bg-ui-controls/50",
!item.selected && active ? "bg-ui-controls/50" : "",
item.selected ? "bg-white/10" : "hover:bg-white/5",
!item.selected && active ? "bg-white/5" : "",
item.disabled ? "!cursor-not-allowed opacity-50" : "",
)}
>
@@ -367,7 +366,7 @@ function RichListRow({
"flex h-9 w-9 shrink-0 items-center justify-center rounded-none border text-base-fg/80 transition-colors",
item.selected
? "border-white bg-white/10"
: "border-ui-controls-border bg-ui-controls/60",
: "border-ui-controls-border bg-white/5",
)}
>
{item.icon}
@@ -411,8 +410,8 @@ function RichListRow({
{rightNode ??
(item.selected &&
(item.selectedRight ?? (
<span className="ml-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-[3px] bg-white">
<CheckIcon className="text-[11px] font-bold text-black" />
<span className="ml-1 flex h-5 w-5 shrink-0 items-center justify-center">
<CheckIcon aria-hidden="true" className="h-4 w-4 text-white" strokeWidth={2.5} />
</span>
)))}
</div>
@@ -634,7 +633,7 @@ function SubmenuFlyout({
transform: "translateY(-50%)",
zIndex: 9999,
}}
className="pointer-events-auto w-[340px] rounded-[3px] border border-ui-panel-border bg-ui-panel p-1.5 text-base-fg"
className="pointer-events-auto w-[340px] rounded-[3px] border border-ui-panel-border bg-ui-controls p-1.5 text-base-fg"
>
<div className="mb-1 mt-0.5 px-1.5 text-sm font-normal text-base-fg opacity-70">
{item.label}
@@ -644,7 +643,7 @@ function SubmenuFlyout({
slow bouncing arrow hinting to scroll up. */}
<div
className={twMerge(
"pointer-events-none absolute inset-x-0 top-0 z-20 flex h-10 items-start justify-center bg-gradient-to-b from-ui-panel to-transparent pt-1 transition-opacity duration-200",
"pointer-events-none absolute inset-x-0 top-0 z-20 flex h-10 items-start justify-center bg-gradient-to-b from-ui-controls to-transparent pt-1 transition-opacity duration-200",
canScrollUp ? "opacity-100" : "opacity-0",
)}
>
@@ -694,7 +693,7 @@ function SubmenuFlyout({
slow bouncing arrow hinting to scroll down. */}
<div
className={twMerge(
"pointer-events-none absolute inset-x-0 bottom-0 z-20 flex h-10 items-end justify-center bg-gradient-to-t from-ui-panel to-transparent pb-1 transition-opacity duration-200",
"pointer-events-none absolute inset-x-0 bottom-0 z-20 flex h-10 items-end justify-center bg-gradient-to-t from-ui-controls to-transparent pb-1 transition-opacity duration-200",
canScrollDown ? "opacity-100" : "opacity-0",
)}
>
@@ -852,9 +851,10 @@ export const PopoverMenu = ({
const className = twMerge(
"text-sm font-medium rounded-[3px] px-2.5 py-1.5",
"flex gap-2 items-center justify-center outline-none",
"flex gap-2 items-center justify-center focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-white/60",
"transition-colors duration-150",
"bg-ui-controls px-3 text-base-fg hover:bg-ui-controls/80 border border-ui-controls-border",
"bg-ui-controls px-3 text-base-fg border border-ui-controls-border",
NEUTRAL_BUTTON_HOVER_CLASSES,
buttonClassName,
);
@@ -1090,7 +1090,7 @@ export const PopoverMenu = ({
<div
ref={panelContentRef}
className={twMerge(
"z-10 min-w-48 mt-2 rounded-[3px] bg-ui-panel p-1.5 border border-ui-panel-border overflow-visible",
"z-10 min-w-48 mt-2 rounded-[3px] bg-ui-controls p-1.5 border border-ui-panel-border overflow-visible",
position === "top" ? "mb-2" : "mt-2",
panelClassName,
)}
@@ -1098,7 +1098,7 @@ export const PopoverMenu = ({
onMouseLeave={() => handlePanelMouseLeave(close)}
>
{panelTitle && (
<div className="mb-2 mt-0.5 flex justify-between px-1.5 text-sm font-normal text-base-fg opacity-70">
<div className="mb-1 flex items-center justify-between border-b border-ui-panel-border px-2 pb-2 pt-1 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
{panelTitle}
{panelActionLabel && (
<button
@@ -1125,7 +1125,7 @@ export const PopoverMenu = ({
with a slow bouncing arrow hinting to scroll up. */}
<div
className={twMerge(
"pointer-events-none absolute inset-x-0 top-0 z-20 flex h-10 items-start justify-center bg-gradient-to-b from-ui-panel to-transparent pt-1 transition-opacity duration-200",
"pointer-events-none absolute inset-x-0 top-0 z-20 flex h-10 items-start justify-center bg-gradient-to-b from-ui-controls to-transparent pt-1 transition-opacity duration-200",
canScrollUp ? "opacity-100" : "opacity-0",
)}
>
@@ -1222,7 +1222,7 @@ export const PopoverMenu = ({
with a slow bouncing arrow hinting to scroll down. */}
<div
className={twMerge(
"pointer-events-none absolute inset-x-0 bottom-0 z-20 flex h-10 items-end justify-center bg-gradient-to-t from-ui-panel to-transparent pb-1 transition-opacity duration-200",
"pointer-events-none absolute inset-x-0 bottom-0 z-20 flex h-10 items-end justify-center bg-gradient-to-t from-ui-controls to-transparent pb-1 transition-opacity duration-200",
canScrollDown ? "opacity-100" : "opacity-0",
)}
>
@@ -1232,7 +1232,7 @@ export const PopoverMenu = ({
) : mode === "hoverSelect" ? (
<div className="relative flex flex-col text-sm text-base-fg overflow-visible">
{maxListHeight && canScrollUp && (
<div className="absolute top-0 left-0 right-0 z-20 flex justify-center bg-gradient-to-b from-ui-panel via-ui-panel/80 to-transparent py-1.5 pointer-events-none">
<div className="absolute top-0 left-0 right-0 z-20 flex justify-center bg-gradient-to-b from-ui-controls via-ui-controls/80 to-transparent py-1.5 pointer-events-none">
<ChevronUpIcon className="text-base-fg/60 text-xs animate-bounce" />
</div>
)}
@@ -1274,9 +1274,9 @@ export const PopoverMenu = ({
"group flex cursor-pointer items-start gap-2 rounded-[3px] px-2 py-2 transition-all",
item.selected
? "bg-white/10 border-l-2 border-white"
: "hover:bg-ui-controls/50",
: "hover:bg-white/5",
!item.selected && openTooltipIdx === index
? "bg-ui-controls/50"
? "bg-white/5"
: "",
item.disabled
? "!cursor-not-allowed opacity-50"
@@ -1333,7 +1333,7 @@ export const PopoverMenu = ({
{item.selected &&
(item.selectedRight ?? (
<span className="text-white text-xl flex items-center justify-center mr-1">
<CircleCheckIcon />
<CheckIcon aria-hidden="true" className="h-4 w-4 shrink-0" strokeWidth={2.5} />
</span>
))}
</div>
@@ -1366,7 +1366,7 @@ export const PopoverMenu = ({
position="right"
delay={item.tooltipDelayMs ?? 300}
interactive
className="!pointer-events-auto z-50 min-w-48 rounded-[3px] bg-ui-panel p-1.5 border border-ui-panel-border"
className="!pointer-events-auto z-50 min-w-48 rounded-[3px] bg-ui-controls p-1.5 border border-ui-panel-border"
onOpenChange={(open) =>
setOpenTooltipIdx((prev) =>
open
@@ -1391,7 +1391,7 @@ export const PopoverMenu = ({
})}
</div>
{maxListHeight && canScrollDown && (
<div className="absolute bottom-0 left-0 right-0 z-20 flex justify-center bg-gradient-to-t from-ui-panel via-ui-panel/80 to-transparent py-1.5 pointer-events-none">
<div className="absolute bottom-0 left-0 right-0 z-20 flex justify-center bg-gradient-to-t from-ui-controls via-ui-controls/80 to-transparent py-1.5 pointer-events-none">
<ChevronDownIcon className="text-base-fg/60 text-xs animate-bounce" />
</div>
)}
@@ -1429,7 +1429,7 @@ export const PopoverMenu = ({
position="right"
delay={item.tooltipDelayMs ?? 1000}
interactive
className="!pointer-events-auto z-50 min-w-48 rounded-[3px] bg-ui-panel p-1.5 border border-ui-panel-border"
className="!pointer-events-auto z-50 min-w-48 rounded-[3px] bg-ui-controls p-1.5 border border-ui-panel-border"
onOpenChange={(open) =>
setOpenTooltipIdx((prev) =>
open ? index : prev === index ? null : prev,
@@ -1440,9 +1440,9 @@ export const PopoverMenu = ({
className={twMerge(
"flex w-full items-center shadow-none justify-between px-1.5",
"font-sans text-sm font-medium normal-case tracking-normal",
"bg-transparent hover:bg-ui-controls/60",
"bg-transparent hover:bg-white/5",
openTooltipIdx === index
? "bg-ui-controls/60"
? "bg-white/5"
: "",
mode === "toggle" && item.selected
? "hover:bg-ui-controls/80"
@@ -1493,15 +1493,10 @@ export const PopoverMenu = ({
{mode === "toggle" && (
<span
className={twMerge(
"ml-2 h-5 w-5 rounded-[3px] border flex items-center justify-center transition-colors",
item.selected
? "border-white bg-white"
: "border-transparent bg-transparent",
)}
className="ml-2 flex h-5 w-5 shrink-0 items-center justify-center"
>
{item.selected && (
<CheckIcon className="text-black text-xs font-bold" />
<CheckIcon aria-hidden="true" className="h-4 w-4 text-white" strokeWidth={2.5} />
)}
</span>
)}
@@ -1512,7 +1507,7 @@ export const PopoverMenu = ({
className={twMerge(
"flex w-full items-center shadow-none justify-between px-1.5",
"font-sans text-sm font-medium normal-case tracking-normal",
"bg-transparent hover:bg-ui-controls/60",
"bg-transparent hover:bg-white/5",
mode === "toggle" && item.selected
? "hover:bg-ui-controls/80"
: "",
@@ -1555,15 +1550,10 @@ export const PopoverMenu = ({
{mode === "toggle" && (
<span
className={twMerge(
"ml-2 h-5 w-5 rounded-[3px] border flex items-center justify-center transition-colors",
item.selected
? "border-white bg-white"
: "border-transparent bg-transparent",
)}
className="ml-2 flex h-5 w-5 shrink-0 items-center justify-center"
>
{item.selected && (
<CheckIcon className="text-black text-xs font-bold" />
<CheckIcon aria-hidden="true" className="h-4 w-4 text-white" strokeWidth={2.5} />
)}
</span>
)}
@@ -1,10 +1,16 @@
.pricing-hud {
font-family: ui-monospace, monospace;
font-size: 10px;
font-family: var(--font-geist-mono, "Geist Mono"), ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.6875rem;
font-weight: 500;
line-height: 1.5;
text-transform: uppercase;
letter-spacing: 0.15em;
}
.pricing-sale-badge {
font-size: 10px;
letter-spacing: 0.1em;
}
.website-plan-card {
--plan: #555;
--plan-ink: #ddd;
@@ -32,7 +32,7 @@ const PLAN_STYLE: Record<
},
};
const PLAN_BUTTON_CLASSES =
"h-11 w-full rounded-none bg-[var(--plan)] hover:bg-[var(--plan)] text-white hover:opacity-90";
"h-11 w-full text-[11px] rounded-none bg-[var(--plan)] hover:bg-[var(--plan)] text-white hover:opacity-90";
const CONTACT_EMAIL = "hello@storyteller.ai";
const ENTERPRISE_FEATURES = [
"Bespoke credit allocation",
@@ -89,9 +89,9 @@ export function WebsitePricingTable({
className="w-fit"
listClassName="rounded-none"
indicatorClassName="rounded-none"
tabClassName="w-24"
tabClassName="w-24 font-mono text-[11px] uppercase tracking-[0.1em]"
/>
<span className="pricing-hud bg-primary px-2 py-1 text-white">
<span className="pricing-hud pricing-sale-badge bg-primary px-2 py-1 text-white">
{PROMO_PCT}% off · ends soon
</span>
<p className="pricing-hud hidden text-primary sm:block">
@@ -1,7 +1,7 @@
import { useState } from "react";
import { Tooltip } from "@storyteller/ui-tooltip";
import { Modal } from "@storyteller/ui-modal";
import { Button } from "@storyteller/ui-button";
import { Button, NEUTRAL_BUTTON_HOVER_CLASSES } from "@storyteller/ui-button";
import { EraserIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
@@ -16,7 +16,7 @@ import { twMerge } from "tailwind-merge";
// `buttonClassName` to PopoverMenu triggers (e.g. the generation count
// picker) so they match this button.
export const PROMPT_TOOLBAR_ICON_BUTTON_CLASSES =
"flex h-9 w-9 items-center justify-center rounded-[3px] border-0 bg-transparent px-0 text-base-fg/50 shadow-none transition-colors hover:bg-base-fg/10 hover:text-base-fg/90";
`flex h-9 w-9 items-center justify-center rounded-[3px] border border-transparent bg-transparent px-0 text-base-fg/50 shadow-none transition-colors ${NEUTRAL_BUTTON_HOVER_CLASSES} hover:text-base-fg/90`;
interface PromptClearAllButtonProps {
onClick: () => void;