Compare commits

...
Author SHA1 Message Date
Hanashi 5d869b9918 fix margins 2026-08-10 23:18:48 -04:00
Hanashi c01f1fd860 fix 4k margins 2026-08-10 22:38:55 -04:00
Hanashi d8dfcedc85 fix for "impossible" condition (we always probe duration, but failsafe) 2026-08-10 22:08:28 -04:00
Hanashi 1fea22b65b margin boost 2026-08-10 22:00:38 -04:00
BFlat 48d2092edb Promptbox: image ref mention chips (#1880)
- `@ImageN` mentions now render as thumbnail chips, same as characters
- Chip menu (Replace / Preview / Remove) works on image chips; Replace
lists same-type refs only
- Chip menu shows type-aware subtitle, empty text, and fallback icon
- Fix: webapp chip Preview showed thumbnail-size image - now passes
full-res `fullUrl`
- Video/audio mentions unchanged (still colored text); website's old
copy untouched
2026-08-10 21:50:49 +07:00
hanashi 865fcb8b56 batch sizes (#1879) 2026-08-10 01:59:09 -04:00
hanashi f563b06614 Cleanup dead enums (#1878) 2026-08-10 01:50:46 -04:00
hanashi 0a84a3ce80 fix codegen + also codegen query cache (#1877) 2026-08-10 00:10:55 -04:00
90 changed files with 2637 additions and 4296 deletions
@@ -9,4 +9,4 @@
"nullable": []
},
"hash": "03919ba8286d18980ecaa0038df76f43f5388db0d3838181fa8f15f4cbb932af"
}
}
@@ -9,4 +9,4 @@
"nullable": []
},
"hash": "258ac40d3fab39f110e1e82d50e73542423bfe9a21f8127e6cd83214c1726e25"
}
}
@@ -110,4 +110,4 @@
]
},
"hash": "62b3e2e058331a6819acd98c634b3bf2d10aa38d43b49ab69fb8637497beec2f"
}
}
@@ -33,4 +33,4 @@
]
},
"hash": "6ca0fcdf495624cf347f612586868f6dea93f52d9f942a92dcd2b3e6aba7c63c"
}
}
@@ -9,4 +9,4 @@
"nullable": []
},
"hash": "96599b4854248d84188b092db84edb50dc5402bb7771fd8ea96a96ad43e732c0"
}
}
@@ -176,4 +176,4 @@
]
},
"hash": "cd04f4f7b48ae9da53b6a210111f8e3edff94dee30263e22b45d5e29dbe9245a"
}
}
@@ -99,4 +99,4 @@
]
},
"hash": "cf2794c87868e77dfaa45bdbe630ae276dbb8c2d041442ba465af0b7a6d61af6"
}
}
@@ -9,4 +9,4 @@
"nullable": []
},
"hash": "d88098343686ded52a71fc45a1b96d28ea3003975d6e7d20eee2022aa31d8189"
}
}
@@ -121,4 +121,4 @@
]
},
"hash": "f1d41ad17cb01f5aef0ce2eea042ccb3e5e649365a0befaf0f46492b75523442"
}
}
@@ -25,8 +25,6 @@ pub enum ApiClientVideoModel {
Seedance2p0Fast,
Seedance2p0BytePlus,
Seedance2p0BytePlusFast,
Seedance2p0Ultra,
Seedance2p0UltraFast,
Seedance2p0BytePlusUltra,
Seedance2p0BytePlusUltraFast,
Seedance2p0Mini,
@@ -65,8 +63,6 @@ impl From<String> for ApiClientVideoModel {
"seedance_2p0_fast" => Self::Seedance2p0Fast,
"seedance_2p0_bp" => Self::Seedance2p0BytePlus,
"seedance_2p0_bp_fast" => Self::Seedance2p0BytePlusFast,
"seedance_2p0_u" => Self::Seedance2p0Ultra,
"seedance_2p0_u_fast" => Self::Seedance2p0UltraFast,
"seedance_2p0_bpu" => Self::Seedance2p0BytePlusUltra,
"seedance_2p0_bpu_fast" => Self::Seedance2p0BytePlusUltraFast,
"seedance_2p0_mini" => Self::Seedance2p0Mini,
@@ -106,8 +102,6 @@ impl From<ApiClientVideoModel> for String {
ApiClientVideoModel::Seedance2p0Fast => "seedance_2p0_fast".to_string(),
ApiClientVideoModel::Seedance2p0BytePlus => "seedance_2p0_bp".to_string(),
ApiClientVideoModel::Seedance2p0BytePlusFast => "seedance_2p0_bp_fast".to_string(),
ApiClientVideoModel::Seedance2p0Ultra => "seedance_2p0_u".to_string(),
ApiClientVideoModel::Seedance2p0UltraFast => "seedance_2p0_u_fast".to_string(),
ApiClientVideoModel::Seedance2p0BytePlusUltra => "seedance_2p0_bpu".to_string(),
ApiClientVideoModel::Seedance2p0BytePlusUltraFast => "seedance_2p0_bpu_fast".to_string(),
ApiClientVideoModel::Seedance2p0Mini => "seedance_2p0_mini".to_string(),
@@ -165,8 +159,6 @@ mod tests {
(ApiClientVideoModel::Seedance2p0Fast, "seedance_2p0_fast"),
(ApiClientVideoModel::Seedance2p0BytePlus, "seedance_2p0_bp"),
(ApiClientVideoModel::Seedance2p0BytePlusFast, "seedance_2p0_bp_fast"),
(ApiClientVideoModel::Seedance2p0Ultra, "seedance_2p0_u"),
(ApiClientVideoModel::Seedance2p0UltraFast, "seedance_2p0_u_fast"),
(ApiClientVideoModel::Seedance2p0BytePlusUltra, "seedance_2p0_bpu"),
(ApiClientVideoModel::Seedance2p0BytePlusUltraFast, "seedance_2p0_bpu_fast"),
(ApiClientVideoModel::Seedance2p0Mini, "seedance_2p0_mini"),
@@ -68,11 +68,7 @@ pub enum RouterVideoModel {
#[serde(rename = "seedance_2p0_bp_fast")]
Seedance2p0BytePlusFast,
#[serde(rename = "seedance_2p0_u")]
Seedance2p0Ultra,
#[serde(rename = "seedance_2p0_u_fast")]
Seedance2p0UltraFast,
#[serde(rename = "seedance_2p0_bpu")]
Seedance2p0BytePlusUltra,
@@ -26,8 +26,6 @@ use crate::generate::generate_video::providers::artcraft::seedance_1p0_lite::bui
use crate::generate::generate_video::providers::artcraft::seedance_1p5_pro::build::build_artcraft_seedance_1p5_pro;
use crate::generate::generate_video::providers::artcraft::seedance_2p0::build::build_artcraft_seedance_2p0;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_fast::build::build_artcraft_seedance_2p0_fast;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_u::build::build_artcraft_seedance_2p0_u;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_u_fast::build::build_artcraft_seedance_2p0_u_fast;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp::build::build_artcraft_seedance_2p0_bp;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_fast::build::build_artcraft_seedance_2p0_bp_fast;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu::build::build_artcraft_seedance_2p0_bpu;
@@ -50,8 +48,6 @@ use crate::generate::generate_video::providers::artcraft::vidu_q3::build::build_
use crate::generate::generate_video::providers::artcraft::vidu_q3_turbo::build::build_artcraft_vidu_q3_turbo;
use crate::generate::generate_video::providers::kinovi::happy_horse_1p0::build::build_kinovi_happy_horse_1p0;
use crate::generate::generate_video::providers::kinovi::seedance_2p0::build::build_kinovi_seedance_2p0;
use crate::generate::generate_video::providers::gmicloud::seedance_2p0_g::build::build_gmicloud_seedance_2p0_u;
use crate::generate::generate_video::providers::gmicloud::seedance_2p0_fast_g::build::build_gmicloud_seedance_2p0_u_fast;
use crate::generate::generate_video::providers::grok_api::grok_imagine_video::build::build_grok_api_grok_imagine_video;
use crate::generate::generate_video::providers::grok_api::grok_imagine_video_1p5::build::build_grok_api_grok_imagine_video_1p5;
use crate::generate::generate_video::providers::fal::kling_1_6_pro::build::build_fal_kling_1_6_pro;
@@ -201,8 +197,6 @@ impl GenerateVideoRequestBuilder {
(RouterProvider::Artcraft, RouterVideoModel::MinimaxH3) => build_artcraft_minimax_h3(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0) => build_artcraft_seedance_2p0(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0Fast) => build_artcraft_seedance_2p0_fast(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0Ultra) => build_artcraft_seedance_2p0_u(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0UltraFast) => build_artcraft_seedance_2p0_u_fast(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlus) => build_artcraft_seedance_2p0_bp(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusFast) => build_artcraft_seedance_2p0_bp_fast(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusUltra) => build_artcraft_seedance_2p0_bpu(self),
@@ -250,9 +244,6 @@ impl GenerateVideoRequestBuilder {
(RouterProvider::Fal, RouterVideoModel::Veo3p1Lite) => build_fal_veo_3p1_lite(self),
(RouterProvider::Fal, RouterVideoModel::ViduQ3) => build_fal_vidu_q3(self),
(RouterProvider::Fal, RouterVideoModel::ViduQ3Turbo) => build_fal_vidu_q3_turbo(self),
// GmiCloud
(RouterProvider::GmiCloud, RouterVideoModel::Seedance2p0Ultra) => build_gmicloud_seedance_2p0_u(self),
(RouterProvider::GmiCloud, RouterVideoModel::Seedance2p0UltraFast) => build_gmicloud_seedance_2p0_u_fast(self),
// Grok
(RouterProvider::GrokApi, RouterVideoModel::GrokImagineVideo) => build_grok_api_grok_imagine_video(self),
(RouterProvider::GrokApi, RouterVideoModel::GrokImagineVideo1p5) => build_grok_api_grok_imagine_video_1p5(self),
@@ -231,7 +231,7 @@ fn plan_output_resolution(
}
}
/// Batch counts: 1, 2, 4.
/// Batch counts: 1 through 4.
pub fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
@@ -239,7 +239,8 @@ pub fn plan_batch_count(
let count = video_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 | 2 | 4 => Ok(count),
1..=4 => Ok(count),
// 5 and above:
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
@@ -248,9 +249,7 @@ pub fn plan_batch_count(
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(4),
RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(if count < 4 { 2 } else { 4 })
}
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(4),
},
}
}
@@ -23,8 +23,6 @@ pub mod seedance_1p0_lite;
pub mod seedance_1p5_pro;
pub mod seedance_2p0;
pub mod seedance_2p0_fast;
pub mod seedance_2p0_u_fast;
pub mod seedance_2p0_u;
pub mod seedance_2p0_bp;
pub mod seedance_2p0_bp_fast;
pub mod seedance_2p0_bpu;
@@ -79,6 +79,12 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 15, 2), 750);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 3), 375);
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 15, 3), 1125);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 4), 500);
@@ -109,6 +115,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 2), 100);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 3), 150);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 4), 200);
@@ -133,6 +144,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5, 2), 500);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5, 3), 750);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5, 4), 1000);
@@ -76,6 +76,12 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 15, 2), 600);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 3), 300);
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 15, 3), 900);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 4), 400);
@@ -106,6 +112,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 2), 90);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 3), 135);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 4), 180);
@@ -65,7 +65,6 @@ impl ArtcraftSeedance2p0CostState {
let usd_cents = seedance_2p0_four_k_usd_cents(
self.duration_seconds,
self.batch_count,
self.has_video_reference,
);
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
@@ -146,6 +145,13 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 15, 2), 480);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 4, 3), 192);
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 3), 240);
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 15, 3), 720);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 4, 4), 256);
@@ -177,6 +183,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 2), 78);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 3), 117);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 4), 155);
@@ -201,6 +212,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5, 2), 466);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5, 3), 699);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5, 4), 933);
@@ -226,10 +242,10 @@ mod tests {
#[test]
fn explicit_4k_without_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, false), 347);
assert_eq!(artcraft_4k_cents(5, 1, false), 433);
assert_eq!(artcraft_4k_cents(10, 1, false), 866);
assert_eq!(artcraft_4k_cents(15, 1, false), 1299);
assert_eq!(artcraft_4k_cents(4, 1, false), 358);
assert_eq!(artcraft_4k_cents(5, 1, false), 447);
assert_eq!(artcraft_4k_cents(10, 1, false), 893);
assert_eq!(artcraft_4k_cents(15, 1, false), 1340);
}
#[test]
@@ -464,4 +480,74 @@ mod tests {
.cost_in_usd_cents
.unwrap()
}
// ── Video reference deltas ──
//
// References price on the with-reference rate card; the delta must be positive at every resolution and duration.
mod video_reference_deltas {
use enums::common::generation::common_resolution::CommonResolution;
use super::super::ArtcraftSeedance2p0CostState;
#[test]
fn refs_cost_more_at_480p() {
assert_ref_delta(CommonResolution::FourEightyP, 5, 39, 45, 6);
assert_ref_delta(CommonResolution::FourEightyP, 10, 78, 89, 11);
assert_ref_delta(CommonResolution::FourEightyP, 15, 117, 133, 16);
}
#[test]
fn refs_cost_more_at_720p() {
assert_ref_delta(CommonResolution::SevenTwentyP, 5, 80, 111, 31);
assert_ref_delta(CommonResolution::SevenTwentyP, 10, 160, 221, 61);
assert_ref_delta(CommonResolution::SevenTwentyP, 15, 240, 331, 91);
}
#[test]
fn refs_cost_more_at_1080p() {
assert_ref_delta(CommonResolution::TenEightyP, 5, 233, 256, 23);
assert_ref_delta(CommonResolution::TenEightyP, 10, 466, 511, 45);
assert_ref_delta(CommonResolution::TenEightyP, 15, 699, 767, 68);
}
#[test]
fn refs_cost_more_at_4k() {
assert_ref_delta(CommonResolution::FourK, 5, 447, 569, 122);
assert_ref_delta(CommonResolution::FourK, 10, 893, 1138, 245);
assert_ref_delta(CommonResolution::FourK, 15, 1340, 1707, 367);
}
/// Price the same generation with and without a reference video; pin
/// both prices and the delta, in USD cents AND credits (credits equal
/// cents, and both must show references costing more).
fn assert_ref_delta(
resolution: CommonResolution,
duration_seconds: u16,
expected_no_ref: u64,
expected_with_ref: u64,
expected_delta: u64,
) {
let no_ref = ArtcraftSeedance2p0CostState {
resolution, duration_seconds, batch_count: 1, has_video_reference: false,
}.estimate_cost();
let with_ref = ArtcraftSeedance2p0CostState {
resolution, duration_seconds, batch_count: 1, has_video_reference: true,
}.estimate_cost();
let no_ref_cents = no_ref.cost_in_usd_cents.unwrap();
let with_ref_cents = with_ref.cost_in_usd_cents.unwrap();
assert_eq!(no_ref_cents, expected_no_ref);
assert_eq!(with_ref_cents, expected_with_ref);
assert_eq!(with_ref_cents - no_ref_cents, expected_delta);
assert!(with_ref_cents > no_ref_cents, "references must cost more");
let no_ref_credits = no_ref.cost_in_credits.unwrap();
let with_ref_credits = with_ref.cost_in_credits.unwrap();
assert_eq!(no_ref_credits, expected_no_ref);
assert_eq!(with_ref_credits, expected_with_ref);
assert_eq!(with_ref_credits - no_ref_credits, expected_delta);
}
}
}
@@ -1,6 +1,5 @@
use enums::common::generation::common_resolution::CommonResolution;
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_four_k_usd_cents;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp::request::ArtcraftSeedance2p0BytePlusRequestState;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
@@ -12,6 +11,24 @@ const CENTS_PER_SECOND_480P: f64 = 10.0;
const CENTS_PER_SECOND_720P: f64 = 25.0;
const CENTS_PER_SECOND_1080P: f64 = 50.0;
/// USD cents per second, in hundredths of a cent, when one or more
/// reference videos are attached. Held as integer hundredths so the math is
/// exact; rounded up to whole cents once, after multiplying by
/// duration × batch.
/// 480p: 10.20 ¢/s
/// 720p: 25.70 ¢/s
/// 1080p: 57.80 ¢/s
/// 4K: 119.00 ¢/s
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_480P: u64 = 1_020;
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_720P: u64 = 2_570;
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_1080P: u64 = 5_780;
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_4K: u64 = 11_900;
/// 4K output price with no reference videos, in hundredths of a USD cent
/// per second (95.00 ¢/s), rounded up to whole cents after multiplying by
/// duration × batch.
const CENTI_CENTS_PER_SECOND_4K: u64 = 9_500;
pub struct ArtcraftSeedance2p0BytePlusCostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
@@ -32,12 +49,24 @@ impl ArtcraftSeedance2p0BytePlusCostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
if self.has_video_reference {
let usd_cents = self.with_video_reference_usd_cents();
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
};
}
if self.resolution == CommonResolution::FourK {
let usd_cents = seedance_2p0_four_k_usd_cents(
self.duration_seconds,
self.batch_count,
self.has_video_reference,
);
let total_centi_cents = CENTI_CENTS_PER_SECOND_4K
* self.duration_seconds as u64
* self.batch_count as u64;
let usd_cents = total_centi_cents.div_ceil(100);
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
@@ -67,6 +96,22 @@ impl ArtcraftSeedance2p0BytePlusCostState {
failures_are_refunded: None,
}
}
/// Price when one or more reference videos are attached.
fn with_video_reference_usd_cents(&self) -> u64 {
let centi_cents_per_second = match self.resolution {
CommonResolution::FourEightyP => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_480P,
CommonResolution::TenEightyP => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_1080P,
CommonResolution::FourK => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_4K,
// Everything else (including 720p) prices at 720p.
_ => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_720P,
};
let seconds = self.duration_seconds as u64 * self.batch_count as u64;
// Round up to whole cents.
(centi_cents_per_second * seconds).div_ceil(100)
}
}
#[cfg(test)]
@@ -92,6 +137,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 2), 250);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 3), 375);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 4), 500);
@@ -119,6 +169,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 2), 100);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 3), 150);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 4), 200);
@@ -141,6 +196,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5, 2), 500);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5, 3), 750);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5, 4), 1000);
@@ -164,18 +224,20 @@ mod tests {
#[test]
fn explicit_4k_without_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, false), 347);
assert_eq!(artcraft_4k_cents(5, 1, false), 433);
assert_eq!(artcraft_4k_cents(10, 1, false), 866);
assert_eq!(artcraft_4k_cents(15, 1, false), 1299);
// 95.00 ¢/s card, rounded up to whole cents.
assert_eq!(artcraft_4k_cents(4, 1, false), 380);
assert_eq!(artcraft_4k_cents(5, 1, false), 475);
assert_eq!(artcraft_4k_cents(10, 1, false), 950);
assert_eq!(artcraft_4k_cents(15, 1, false), 1425);
}
#[test]
fn explicit_4k_with_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, true), 416);
assert_eq!(artcraft_4k_cents(5, 1, true), 519);
assert_eq!(artcraft_4k_cents(10, 1, true), 1038);
assert_eq!(artcraft_4k_cents(15, 1, true), 1557);
// 119.00 ¢/s with-reference card, rounded up to whole cents.
assert_eq!(artcraft_4k_cents(4, 1, true), 476);
assert_eq!(artcraft_4k_cents(5, 1, true), 595);
assert_eq!(artcraft_4k_cents(10, 1, true), 1190);
assert_eq!(artcraft_4k_cents(15, 1, true), 1785);
}
}
@@ -228,4 +290,74 @@ mod tests {
fn cost_cents(resolution: Option<RouterResolution>, duration_seconds: u16, video_batch_count: u16) -> u64 {
build_cost(resolution, duration_seconds, video_batch_count).cost_in_usd_cents.unwrap()
}
// ── Video reference deltas ──
//
// References price on the with-reference rate card (10.20/25.70/57.80/113.80 ¢/s); the delta must be positive at every resolution and duration.
mod video_reference_deltas {
use enums::common::generation::common_resolution::CommonResolution;
use super::super::ArtcraftSeedance2p0BytePlusCostState;
#[test]
fn refs_cost_more_at_480p() {
assert_ref_delta(CommonResolution::FourEightyP, 5, 50, 51, 1);
assert_ref_delta(CommonResolution::FourEightyP, 10, 100, 102, 2);
assert_ref_delta(CommonResolution::FourEightyP, 15, 150, 153, 3);
}
#[test]
fn refs_cost_more_at_720p() {
assert_ref_delta(CommonResolution::SevenTwentyP, 5, 125, 129, 4);
assert_ref_delta(CommonResolution::SevenTwentyP, 10, 250, 257, 7);
assert_ref_delta(CommonResolution::SevenTwentyP, 15, 375, 386, 11);
}
#[test]
fn refs_cost_more_at_1080p() {
assert_ref_delta(CommonResolution::TenEightyP, 5, 250, 289, 39);
assert_ref_delta(CommonResolution::TenEightyP, 10, 500, 578, 78);
assert_ref_delta(CommonResolution::TenEightyP, 15, 750, 867, 117);
}
#[test]
fn refs_cost_more_at_4k() {
assert_ref_delta(CommonResolution::FourK, 5, 475, 595, 120);
assert_ref_delta(CommonResolution::FourK, 10, 950, 1190, 240);
assert_ref_delta(CommonResolution::FourK, 15, 1425, 1785, 360);
}
/// Price the same generation with and without a reference video; pin
/// both prices and the delta, in USD cents AND credits (credits equal
/// cents, and both must show references costing more).
fn assert_ref_delta(
resolution: CommonResolution,
duration_seconds: u16,
expected_no_ref: u64,
expected_with_ref: u64,
expected_delta: u64,
) {
let no_ref = ArtcraftSeedance2p0BytePlusCostState {
resolution, duration_seconds, batch_count: 1, has_video_reference: false,
}.estimate_cost();
let with_ref = ArtcraftSeedance2p0BytePlusCostState {
resolution, duration_seconds, batch_count: 1, has_video_reference: true,
}.estimate_cost();
let no_ref_cents = no_ref.cost_in_usd_cents.unwrap();
let with_ref_cents = with_ref.cost_in_usd_cents.unwrap();
assert_eq!(no_ref_cents, expected_no_ref);
assert_eq!(with_ref_cents, expected_with_ref);
assert_eq!(with_ref_cents - no_ref_cents, expected_delta);
assert!(with_ref_cents > no_ref_cents, "references must cost more");
let no_ref_credits = no_ref.cost_in_credits.unwrap();
let with_ref_credits = with_ref.cost_in_credits.unwrap();
assert_eq!(no_ref_credits, expected_no_ref);
assert_eq!(with_ref_credits, expected_with_ref);
assert_eq!(with_ref_credits - no_ref_credits, expected_delta);
}
}
}
@@ -9,10 +9,20 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_fast::
const CENTS_PER_SECOND_480P: f64 = 9.0;
const CENTS_PER_SECOND_720P: f64 = 20.0;
/// USD cents per second, in hundredths of a cent, when one or more
/// reference videos are attached. Held as integer hundredths so the math is
/// exact; rounded up to whole cents once, after multiplying by
/// duration × batch.
/// 480p: 9.65 ¢/s
/// 720p: 20.50 ¢/s
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_480P: u64 = 965;
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_720P: u64 = 2_050;
pub struct ArtcraftSeedance2p0BytePlusFastCostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
pub batch_count: u16,
pub has_video_reference: bool,
}
impl ArtcraftSeedance2p0BytePlusFastCostState {
@@ -21,10 +31,26 @@ impl ArtcraftSeedance2p0BytePlusFastCostState {
.unwrap_or(CommonResolution::SevenTwentyP);
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
let batch_count = request.request.video_batch_count.unwrap_or(1);
Self { resolution, duration_seconds, batch_count }
let has_video_reference = request.request.reference_video_media_tokens
.as_ref()
.is_some_and(|tokens| !tokens.is_empty());
Self { resolution, duration_seconds, batch_count, has_video_reference }
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
if self.has_video_reference {
let usd_cents = self.with_video_reference_usd_cents();
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
};
}
let cents_per_second = match self.resolution {
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
_ => CENTS_PER_SECOND_720P,
@@ -42,6 +68,20 @@ impl ArtcraftSeedance2p0BytePlusFastCostState {
failures_are_refunded: None,
}
}
/// Price when one or more reference videos are attached.
fn with_video_reference_usd_cents(&self) -> u64 {
let centi_cents_per_second = match self.resolution {
CommonResolution::FourEightyP => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_480P,
// Everything else (including 720p) prices at 720p.
_ => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_720P,
};
let seconds = self.duration_seconds as u64 * self.batch_count as u64;
// Round up to whole cents.
(centi_cents_per_second * seconds).div_ceil(100)
}
}
#[cfg(test)]
@@ -67,6 +107,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 2), 200);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 3), 300);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 4), 400);
@@ -94,6 +139,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 2), 90);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 3), 135);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 4), 180);
@@ -65,18 +65,18 @@ mod tests {
#[test]
fn without_video_reference() {
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, false), 14);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 17);
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, false), 33);
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, false), 50);
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, false), 15);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 18);
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, false), 36);
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, false), 54);
}
#[test]
fn with_video_reference() {
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, true), 17);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, true), 21);
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, true), 42);
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, true), 63);
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, true), 18);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, true), 23);
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, true), 45);
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, true), 68);
}
}
@@ -85,18 +85,18 @@ mod tests {
#[test]
fn without_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, false), 35);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 44);
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, false), 88);
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, false), 131);
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, false), 37);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 46);
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, false), 91);
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, false), 137);
}
#[test]
fn with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, true), 42);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, true), 53);
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, true), 105);
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, true), 158);
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, true), 44);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, true), 55);
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, true), 110);
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, true), 165);
}
#[test]
@@ -112,31 +112,31 @@ mod tests {
/// apply). Once, batch 8 executed in full but billed as batch 4.
#[test]
fn every_batch_size_720p_5s() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 44);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 2, false), 88);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 3, false), 131);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, false), 175);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 5, false), 219);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 6, false), 262);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 7, false), 306);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 349);
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]
fn every_batch_size_480p_5s() {
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 17);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 2, false), 33);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 3, false), 50);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 4, false), 66);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 5, false), 82);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 6, false), 99);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 7, false), 115);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 8, false), 131);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 18);
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), 419);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, true), 438);
}
#[test]
@@ -1,6 +1,5 @@
use enums::common::generation::common_resolution::CommonResolution;
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_four_k_usd_cents;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu::request::ArtcraftSeedance2p0BytePlusUltraRequestState;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
@@ -12,6 +11,24 @@ const CENTS_PER_SECOND_480P: f64 = 10.0;
const CENTS_PER_SECOND_720P: f64 = 25.0;
const CENTS_PER_SECOND_1080P: f64 = 50.0;
/// USD cents per second, in hundredths of a cent, when one or more
/// reference videos are attached. Held as integer hundredths so the math is
/// exact; rounded up to whole cents once, after multiplying by
/// duration × batch.
/// 480p: 10.20 ¢/s
/// 720p: 25.70 ¢/s
/// 1080p: 57.80 ¢/s
/// 4K: 119.00 ¢/s
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_480P: u64 = 1_020;
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_720P: u64 = 2_570;
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_1080P: u64 = 5_780;
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_4K: u64 = 11_900;
/// 4K output price with no reference videos, in hundredths of a USD cent
/// per second (95.00 ¢/s), rounded up to whole cents after multiplying by
/// duration × batch.
const CENTI_CENTS_PER_SECOND_4K: u64 = 9_500;
pub struct ArtcraftSeedance2p0BytePlusUltraCostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
@@ -32,12 +49,24 @@ impl ArtcraftSeedance2p0BytePlusUltraCostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
if self.has_video_reference {
let usd_cents = self.with_video_reference_usd_cents();
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
};
}
if self.resolution == CommonResolution::FourK {
let usd_cents = seedance_2p0_four_k_usd_cents(
self.duration_seconds,
self.batch_count,
self.has_video_reference,
);
let total_centi_cents = CENTI_CENTS_PER_SECOND_4K
* self.duration_seconds as u64
* self.batch_count as u64;
let usd_cents = total_centi_cents.div_ceil(100);
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
@@ -67,6 +96,22 @@ impl ArtcraftSeedance2p0BytePlusUltraCostState {
failures_are_refunded: None,
}
}
/// Price when one or more reference videos are attached.
fn with_video_reference_usd_cents(&self) -> u64 {
let centi_cents_per_second = match self.resolution {
CommonResolution::FourEightyP => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_480P,
CommonResolution::TenEightyP => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_1080P,
CommonResolution::FourK => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_4K,
// Everything else (including 720p) prices at 720p.
_ => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_720P,
};
let seconds = self.duration_seconds as u64 * self.batch_count as u64;
// Round up to whole cents.
(centi_cents_per_second * seconds).div_ceil(100)
}
}
#[cfg(test)]
@@ -92,6 +137,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 2), 250);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 3), 375);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 4), 500);
@@ -119,6 +169,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 2), 100);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 3), 150);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 4), 200);
@@ -141,6 +196,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5, 2), 500);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5, 3), 750);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5, 4), 1000);
@@ -164,18 +224,20 @@ mod tests {
#[test]
fn explicit_4k_without_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, false), 347);
assert_eq!(artcraft_4k_cents(5, 1, false), 433);
assert_eq!(artcraft_4k_cents(10, 1, false), 866);
assert_eq!(artcraft_4k_cents(15, 1, false), 1299);
// 95.00 ¢/s card, rounded up to whole cents.
assert_eq!(artcraft_4k_cents(4, 1, false), 380);
assert_eq!(artcraft_4k_cents(5, 1, false), 475);
assert_eq!(artcraft_4k_cents(10, 1, false), 950);
assert_eq!(artcraft_4k_cents(15, 1, false), 1425);
}
#[test]
fn explicit_4k_with_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, true), 416);
assert_eq!(artcraft_4k_cents(5, 1, true), 519);
assert_eq!(artcraft_4k_cents(10, 1, true), 1038);
assert_eq!(artcraft_4k_cents(15, 1, true), 1557);
// 119.00 ¢/s with-reference card, rounded up to whole cents.
assert_eq!(artcraft_4k_cents(4, 1, true), 476);
assert_eq!(artcraft_4k_cents(5, 1, true), 595);
assert_eq!(artcraft_4k_cents(10, 1, true), 1190);
assert_eq!(artcraft_4k_cents(15, 1, true), 1785);
}
}
@@ -228,4 +290,74 @@ mod tests {
fn cost_cents(resolution: Option<RouterResolution>, duration_seconds: u16, video_batch_count: u16) -> u64 {
build_cost(resolution, duration_seconds, video_batch_count).cost_in_usd_cents.unwrap()
}
// ── Video reference deltas ──
//
// References price on the with-reference rate card (10.20/25.70/57.80/113.80 ¢/s); the delta must be positive at every resolution and duration.
mod video_reference_deltas {
use enums::common::generation::common_resolution::CommonResolution;
use super::super::ArtcraftSeedance2p0BytePlusUltraCostState;
#[test]
fn refs_cost_more_at_480p() {
assert_ref_delta(CommonResolution::FourEightyP, 5, 50, 51, 1);
assert_ref_delta(CommonResolution::FourEightyP, 10, 100, 102, 2);
assert_ref_delta(CommonResolution::FourEightyP, 15, 150, 153, 3);
}
#[test]
fn refs_cost_more_at_720p() {
assert_ref_delta(CommonResolution::SevenTwentyP, 5, 125, 129, 4);
assert_ref_delta(CommonResolution::SevenTwentyP, 10, 250, 257, 7);
assert_ref_delta(CommonResolution::SevenTwentyP, 15, 375, 386, 11);
}
#[test]
fn refs_cost_more_at_1080p() {
assert_ref_delta(CommonResolution::TenEightyP, 5, 250, 289, 39);
assert_ref_delta(CommonResolution::TenEightyP, 10, 500, 578, 78);
assert_ref_delta(CommonResolution::TenEightyP, 15, 750, 867, 117);
}
#[test]
fn refs_cost_more_at_4k() {
assert_ref_delta(CommonResolution::FourK, 5, 475, 595, 120);
assert_ref_delta(CommonResolution::FourK, 10, 950, 1190, 240);
assert_ref_delta(CommonResolution::FourK, 15, 1425, 1785, 360);
}
/// Price the same generation with and without a reference video; pin
/// both prices and the delta, in USD cents AND credits (credits equal
/// cents, and both must show references costing more).
fn assert_ref_delta(
resolution: CommonResolution,
duration_seconds: u16,
expected_no_ref: u64,
expected_with_ref: u64,
expected_delta: u64,
) {
let no_ref = ArtcraftSeedance2p0BytePlusUltraCostState {
resolution, duration_seconds, batch_count: 1, has_video_reference: false,
}.estimate_cost();
let with_ref = ArtcraftSeedance2p0BytePlusUltraCostState {
resolution, duration_seconds, batch_count: 1, has_video_reference: true,
}.estimate_cost();
let no_ref_cents = no_ref.cost_in_usd_cents.unwrap();
let with_ref_cents = with_ref.cost_in_usd_cents.unwrap();
assert_eq!(no_ref_cents, expected_no_ref);
assert_eq!(with_ref_cents, expected_with_ref);
assert_eq!(with_ref_cents - no_ref_cents, expected_delta);
assert!(with_ref_cents > no_ref_cents, "references must cost more");
let no_ref_credits = no_ref.cost_in_credits.unwrap();
let with_ref_credits = with_ref.cost_in_credits.unwrap();
assert_eq!(no_ref_credits, expected_no_ref);
assert_eq!(with_ref_credits, expected_with_ref);
assert_eq!(with_ref_credits - no_ref_credits, expected_delta);
}
}
}
@@ -9,10 +9,20 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu_fast:
const CENTS_PER_SECOND_480P: f64 = 9.0;
const CENTS_PER_SECOND_720P: f64 = 20.0;
/// USD cents per second, in hundredths of a cent, when one or more
/// reference videos are attached. Held as integer hundredths so the math is
/// exact; rounded up to whole cents once, after multiplying by
/// duration × batch.
/// 480p: 9.65 ¢/s
/// 720p: 20.50 ¢/s
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_480P: u64 = 965;
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_720P: u64 = 2_050;
pub struct ArtcraftSeedance2p0BytePlusUltraFastCostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
pub batch_count: u16,
pub has_video_reference: bool,
}
impl ArtcraftSeedance2p0BytePlusUltraFastCostState {
@@ -21,10 +31,26 @@ impl ArtcraftSeedance2p0BytePlusUltraFastCostState {
.unwrap_or(CommonResolution::SevenTwentyP);
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
let batch_count = request.request.video_batch_count.unwrap_or(1);
Self { resolution, duration_seconds, batch_count }
let has_video_reference = request.request.reference_video_media_tokens
.as_ref()
.is_some_and(|tokens| !tokens.is_empty());
Self { resolution, duration_seconds, batch_count, has_video_reference }
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
if self.has_video_reference {
let usd_cents = self.with_video_reference_usd_cents();
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
};
}
let cents_per_second = match self.resolution {
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
_ => CENTS_PER_SECOND_720P,
@@ -42,6 +68,20 @@ impl ArtcraftSeedance2p0BytePlusUltraFastCostState {
failures_are_refunded: None,
}
}
/// Price when one or more reference videos are attached.
fn with_video_reference_usd_cents(&self) -> u64 {
let centi_cents_per_second = match self.resolution {
CommonResolution::FourEightyP => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_480P,
// Everything else (including 720p) prices at 720p.
_ => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_720P,
};
let seconds = self.duration_seconds as u64 * self.batch_count as u64;
// Round up to whole cents.
(centi_cents_per_second * seconds).div_ceil(100)
}
}
#[cfg(test)]
@@ -67,6 +107,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 2), 200);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 3), 300);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 4), 400);
@@ -94,6 +139,11 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 2), 90);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 3), 135);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 4), 180);
@@ -65,18 +65,18 @@ mod tests {
#[test]
fn without_video_reference() {
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, false), 14);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 17);
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, false), 33);
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, false), 50);
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, false), 15);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 18);
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, false), 36);
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, false), 54);
}
#[test]
fn with_video_reference() {
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, true), 17);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, true), 21);
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, true), 42);
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, true), 63);
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, true), 18);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, true), 23);
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, true), 45);
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, true), 68);
}
}
@@ -85,18 +85,18 @@ mod tests {
#[test]
fn without_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, false), 35);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 44);
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, false), 88);
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, false), 131);
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, false), 37);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 46);
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, false), 91);
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, false), 137);
}
#[test]
fn with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, true), 42);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, true), 53);
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, true), 105);
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, true), 158);
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, true), 44);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, true), 55);
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, true), 110);
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, true), 165);
}
#[test]
@@ -112,31 +112,31 @@ mod tests {
/// apply). Once, batch 8 executed in full but billed as batch 4.
#[test]
fn every_batch_size_720p_5s() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 44);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 2, false), 88);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 3, false), 131);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, false), 175);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 5, false), 219);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 6, false), 262);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 7, false), 306);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 349);
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]
fn every_batch_size_480p_5s() {
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 17);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 2, false), 33);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 3, false), 50);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 4, false), 66);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 5, false), 82);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 6, false), 99);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 7, false), 115);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 8, false), 131);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 18);
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), 419);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, true), 438);
}
#[test]
@@ -13,7 +13,7 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p0_fast::req
///
/// Kept as f64 because per-second rates are fractional; rounding happens
/// once at the end after multiplying by duration * batch.
const CENTS_PER_SECOND_480P: f64 = 5.181;
const CENTS_PER_SECOND_480P: f64 = 6.40;
const CENTS_PER_SECOND_720P: f64 = 12.727;
/// USD cents per second, in hundredths of a cent, when one or more
@@ -122,6 +122,13 @@ mod tests {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 15, 2), 382);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 4, 3), 153);
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 3), 191);
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 15, 3), 573);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 4, 4), 204);
@@ -142,20 +149,25 @@ mod tests {
#[test]
fn batch_1() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 4, 1), 21);
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 1), 26);
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 10, 1), 52);
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 15, 1), 78);
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 4, 1), 26);
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 1), 32);
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 10, 1), 64);
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 15, 1), 96);
}
#[test]
fn batch_2() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 2), 52);
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 2), 64);
}
#[test]
fn batch_3() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 3), 96);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 4), 104);
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 4), 128);
}
}
@@ -65,18 +65,18 @@ mod tests {
#[test]
fn without_video_reference() {
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, false), 13);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 17);
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, false), 33);
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, false), 49);
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, false), 14);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 18);
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, false), 35);
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, false), 52);
}
#[test]
fn with_video_reference() {
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, true), 17);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, true), 21);
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, true), 42);
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, true), 62);
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, true), 18);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, true), 22);
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, true), 44);
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, true), 66);
}
}
@@ -85,18 +85,18 @@ mod tests {
#[test]
fn without_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, false), 35);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 44);
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, false), 87);
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, false), 130);
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, false), 36);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 45);
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, false), 89);
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, false), 134);
}
#[test]
fn with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, true), 42);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, true), 52);
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, true), 104);
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, true), 156);
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, true), 43);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, true), 54);
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, true), 108);
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, true), 161);
}
#[test]
@@ -112,31 +112,31 @@ mod tests {
/// apply). Once, batch 8 executed in full but billed as batch 4.
#[test]
fn every_batch_size_720p_5s() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 44);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 2, false), 87);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 3, false), 130);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, false), 173);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 5, false), 217);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 6, false), 260);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 7, false), 303);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 346);
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]
fn every_batch_size_480p_5s() {
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 17);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 2, false), 33);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 3, false), 49);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 4, false), 65);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 5, false), 82);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 6, false), 98);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 7, false), 114);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 8, false), 130);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 18);
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), 415);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, true), 429);
}
#[test]
@@ -1,189 +0,0 @@
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
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, SupportedResolutions, UltraWideSupport,
};
use crate::generate::generate_video::providers::artcraft::seedance_2p0_u::request::ArtcraftSeedance2p0UltraRequestState;
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_u(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Seedance2p0Ultra,
SupportedResolutions::FullWith4k,
UltraWideSupport::Supported,
)?;
let state = ArtcraftSeedance2p0UltraRequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0Ultra(state)))
}
#[cfg(test)]
mod tests {
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum;
use crate::api::router_bitrate::RouterBitrate;
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::character_list_ref::CharacterListRef;
use crate::api::router_resolution::RouterResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
use crate::api::router_provider::RouterProvider;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
use super::*;
// ── Field conversions ──
mod field_conversions {
use super::*;
#[test]
fn model_is_seedance_2p0() {
let req = unwrap_request(make_builder(|_| {}));
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0Ultra)));
}
#[test]
fn prompt_passed_through() {
let req = unwrap_request(make_builder(|b| { b.prompt = Some("test".to_string()); }));
assert_eq!(req.request.prompt, Some("test".to_string()));
}
#[test]
fn duration_passed_through() {
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); }));
assert_eq!(req.request.duration_seconds, Some(10));
}
#[test]
fn duration_clamped_to_max() {
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); }));
assert_eq!(req.request.duration_seconds, Some(15));
}
#[test]
fn batch_count_passed_through() {
let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); }));
assert_eq!(req.request.video_batch_count, Some(4));
}
#[test]
fn bitrate_passed_through() {
let req = unwrap_request(make_builder(|b| { b.bitrate = Some(RouterBitrate::High); }));
assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High));
}
}
// ── Resolution ──
mod resolution_tests {
use super::*;
#[test]
fn res_480p() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourEightyP); }));
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
}
#[test]
fn res_720p() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::SevenTwentyP); }));
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
}
#[test]
fn res_1080p() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::TenEightyP); }));
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
}
#[test]
fn res_4k() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourK); }));
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourK));
}
#[test]
fn none_stays_none() {
let req = unwrap_request(make_builder(|_| {}));
assert!(req.request.resolution.is_none());
}
}
// ── Media tokens ──
mod media_token_tests {
use super::*;
#[test]
fn start_frame_token_passed_through() {
let token = MediaFileToken::new("mf_start".to_string());
let req = unwrap_request(make_builder(|b| {
b.start_frame = Some(ImageRef::MediaFileToken(token.clone()));
}));
assert_eq!(req.request.start_frame_image_media_token, Some(token));
}
#[test]
fn url_start_frame_rejected() {
let result = build_artcraft_seedance_2p0_u(GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
..base_builder()
});
assert!(result.is_err());
}
#[test]
fn reference_image_tokens_passed_through() {
let tokens = vec![MediaFileToken::new("mf_a".to_string()), MediaFileToken::new("mf_b".to_string())];
let req = unwrap_request(make_builder(|b| {
b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone()));
}));
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
}
#[test]
fn character_tokens_passed_through() {
let tokens = vec![CharacterToken::new("char_a".to_string()), CharacterToken::new("char_b".to_string())];
let req = unwrap_request(make_builder(|b| {
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone()));
}));
assert_eq!(req.request.reference_character_tokens, Some(tokens));
}
}
// ── Helpers ──
fn base_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
provider: RouterProvider::Artcraft,
duration_seconds: Some(5),
video_batch_count: Some(1),
..Default::default()
}
}
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
let mut builder = base_builder();
f(&mut builder);
builder
}
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p0UltraRequestState {
let result = build_artcraft_seedance_2p0_u(builder).expect("build should succeed");
match result {
VideoGenerationDraftOrRequest::Request(
VideoGenerationRequest::ArtcraftSeedance2p0Ultra(state)
) => state,
_ => panic!("expected ArtcraftSeedance2p0 request"),
}
}
}
@@ -1,237 +0,0 @@
use enums::common::generation::common_resolution::CommonResolution;
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_four_k_usd_cents;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_u::request::ArtcraftSeedance2p0UltraRequestState;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
// ── Pricing constants ──
const CENTS_PER_SECOND_480P: f64 = 5.4404;
const CENTS_PER_SECOND_720P: f64 = 11.2;
const CENTS_PER_SECOND_1080P: f64 = 32.6424;
pub struct ArtcraftSeedance2p0UltraCostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
pub batch_count: u16,
pub has_video_reference: bool,
}
impl ArtcraftSeedance2p0UltraCostState {
pub fn from_request(request: &ArtcraftSeedance2p0UltraRequestState) -> Self {
let resolution = request.request.resolution
.unwrap_or(CommonResolution::SevenTwentyP);
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
let batch_count = request.request.video_batch_count.unwrap_or(1);
let has_video_reference = request.request.reference_video_media_tokens
.as_ref()
.is_some_and(|tokens| !tokens.is_empty());
Self { resolution, duration_seconds, batch_count, has_video_reference }
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
if self.resolution == CommonResolution::FourK {
let usd_cents = seedance_2p0_four_k_usd_cents(
self.duration_seconds,
self.batch_count,
self.has_video_reference,
);
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
};
}
let cents_per_second = match self.resolution {
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
CommonResolution::TenEightyP => CENTS_PER_SECOND_1080P,
_ => CENTS_PER_SECOND_720P,
};
let cents_per_video = (cents_per_second * self.duration_seconds as f64).ceil() as u64;
let usd_cents = cents_per_video * self.batch_count as u64;
VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
#[cfg(test)]
mod tests {
use crate::api::router_provider::RouterProvider;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
mod pricing_720p {
use super::*;
#[test]
fn batch_1() {
// 11.2 * 5 = 56¢
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 1), 56);
// 11.2 * 10 = 112¢
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 10, 1), 112);
// 11.2 * 15 = 168¢
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 15, 1), 168);
}
#[test]
fn batch_2() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 2), 112);
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 10, 2), 224);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 4), 224);
}
#[test]
fn none_defaults_to_720p() {
assert_eq!(cost_cents(None, 10, 1), cost_cents(Some(RouterResolution::SevenTwentyP), 10, 1));
}
}
mod pricing_480p {
use super::*;
#[test]
fn batch_1() {
// 5.4404 * 5 = 27.202 → ceil = 28¢
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 1), 28);
// 5.4404 * 10 = 54.404 → ceil = 55¢
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 10, 1), 55);
// 5.4404 * 15 = 81.606 → ceil = 82¢
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 15, 1), 82);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 10, 4), 220);
}
}
mod pricing_1080p {
use super::*;
#[test]
fn batch_1() {
// 32.6424 * 5 = 163.212 → ceil = 164¢
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5, 1), 164);
// 32.6424 * 10 = 326.424 → ceil = 327¢
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 10, 1), 327);
// 32.6424 * 15 = 489.636 → ceil = 490¢
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 15, 1), 490);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 10, 4), 1308);
}
}
mod four_k_pricing {
use enums::common::generation::common_resolution::CommonResolution;
fn artcraft_4k_cents(duration_seconds: u16, batch_count: u16, has_video_reference: bool) -> u64 {
super::super::ArtcraftSeedance2p0UltraCostState {
resolution: CommonResolution::FourK,
duration_seconds,
batch_count,
has_video_reference,
}
.estimate_cost()
.cost_in_usd_cents
.unwrap()
}
#[test]
fn explicit_4k_without_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, false), 347);
assert_eq!(artcraft_4k_cents(5, 1, false), 433);
assert_eq!(artcraft_4k_cents(10, 1, false), 866);
assert_eq!(artcraft_4k_cents(15, 1, false), 1299);
}
#[test]
fn explicit_4k_with_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, true), 416);
assert_eq!(artcraft_4k_cents(5, 1, true), 519);
assert_eq!(artcraft_4k_cents(10, 1, true), 1038);
assert_eq!(artcraft_4k_cents(15, 1, true), 1557);
}
}
mod relative_pricing {
use super::*;
#[test]
fn cost_480p_cheaper_than_720p_cheaper_than_1080p() {
let c480 = cost_cents(Some(RouterResolution::FourEightyP), 10, 1);
let c720 = cost_cents(Some(RouterResolution::SevenTwentyP), 10, 1);
let c1080 = cost_cents(Some(RouterResolution::TenEightyP), 10, 1);
assert!(c480 < c720);
assert!(c720 < c1080);
}
#[test]
fn cost_scales_with_duration() {
let c5 = cost_cents(None, 5, 1);
let c10 = cost_cents(None, 10, 1);
let c15 = cost_cents(None, 15, 1);
assert!(c5 < c10);
assert!(c10 < c15);
}
#[test]
fn cost_scales_with_batch() {
let b1 = cost_cents(None, 10, 1);
let b2 = cost_cents(None, 10, 2);
let b4 = cost_cents(None, 10, 4);
assert!(b1 < b2);
assert!(b2 < b4);
}
#[test]
fn credits_equal_usd_cents() {
let cost = build_cost(None, 10, 1);
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
}
}
// -- Helpers --
fn build_cost(
resolution: Option<RouterResolution>,
duration_seconds: u16,
video_batch_count: u16,
) -> crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate {
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Ultra,
provider: RouterProvider::Artcraft,
resolution,
duration_seconds: Some(duration_seconds),
video_batch_count: Some(video_batch_count),
..Default::default()
};
builder.build2().expect("build2").estimate_cost().expect("estimate_cost")
}
fn cost_cents(resolution: Option<RouterResolution>, duration_seconds: u16, batch: u16) -> u64 {
build_cost(resolution, duration_seconds, batch).cost_in_usd_cents.unwrap()
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,237 +0,0 @@
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
use crate::generate::generate_video::providers::artcraft::request_common::send_artcraft_omni_video_request;
#[derive(Clone, Debug)]
pub struct ArtcraftSeedance2p0UltraRequestState {
pub request: OmniGenVideoCostAndGenerateRequest,
}
impl ArtcraftSeedance2p0UltraRequestState {
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
send_artcraft_omni_video_request(&self.request, client).await
}
}
#[cfg(test)]
mod tests {
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::character_list_ref::CharacterListRef;
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_resolution::RouterResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
use crate::api::router_provider::RouterProvider;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_client::RouterClient;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_client::utils::api_host::ApiHost;
use test_data::web::character_tokens::{JIM, KNIGHT};
use test_data::web::image_media_tokens::{
FOREST_BACKDROP_PRODUCTION_MEDIA_TOKEN,
JUNO_AT_LAKE_PRODUCTION_MEDIA_TOKEN,
WHITE_HOUSE_SUNSET_PRODUCTION_MEDIA_TOKEN,
};
// -- Aspect ratio tests --
mod aspect_ratio_tests {
use super::*;
#[tokio::test]
#[ignore]
async fn landscape() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A corgi running through a field of wildflowers at sunset.".to_string()),
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..artcraft_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore]
async fn portrait() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A cat sitting on a windowsill watching rain.".to_string()),
aspect_ratio: Some(RouterAspectRatio::TallNineBySixteen),
..artcraft_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore]
async fn square() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A hummingbird hovering near a flower.".to_string()),
aspect_ratio: Some(RouterAspectRatio::Square),
..artcraft_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
// -- Resolution tests --
mod resolution_tests {
use super::*;
#[tokio::test]
#[ignore]
async fn res_480p() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A shiba inu playing in autumn leaves.".to_string()),
resolution: Some(RouterResolution::FourEightyP),
..artcraft_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore]
async fn res_720p() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A golden retriever catching a frisbee on the beach.".to_string()),
resolution: Some(RouterResolution::SevenTwentyP),
..artcraft_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore]
async fn res_1080p() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A fox walking through a snowy forest.".to_string()),
resolution: Some(RouterResolution::TenEightyP),
..artcraft_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
// -- Modality tests --
mod modality_tests {
use super::*;
use test_data::web::image_media_tokens::TALL_MOCHI_WITH_GLASSES_PRODUCTION_MEDIA_TOKEN;
#[tokio::test]
#[ignore]
async fn text_to_video() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A whale breaching in the open ocean at dawn, cinematic.".to_string()),
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..artcraft_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore]
async fn keyframe_start_and_end_frame() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("The dog walks from the lake to the forest.".to_string()),
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new(JUNO_AT_LAKE_PRODUCTION_MEDIA_TOKEN.to_string()))),
end_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new(FOREST_BACKDROP_PRODUCTION_MEDIA_TOKEN.to_string()))),
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..artcraft_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore]
async fn image_references() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("The dog in @2 runs through the scenery in @1 towards the building in @3.".to_string()),
reference_images: Some(ImageListRef::MediaFileTokens(vec![
MediaFileToken::new(FOREST_BACKDROP_PRODUCTION_MEDIA_TOKEN.to_string()),
MediaFileToken::new(TALL_MOCHI_WITH_GLASSES_PRODUCTION_MEDIA_TOKEN.to_string()),
MediaFileToken::new(WHITE_HOUSE_SUNSET_PRODUCTION_MEDIA_TOKEN.to_string()),
])),
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..artcraft_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore]
async fn character_references() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("@Jim and @Knight are sparring in a medieval arena.".to_string()),
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
CharacterToken::new(JIM.token.to_string()),
CharacterToken::new(KNIGHT.token.to_string()),
])),
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..artcraft_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
// -- Helpers --
fn artcraft_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
provider: RouterProvider::Artcraft,
duration_seconds: Some(4),
video_batch_count: Some(1),
..Default::default()
}
}
fn get_artcraft_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
.expect("Failed to read artcraft cookies");
let cookies = cookies.trim().to_string();
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
.expect("Failed to parse cookies")
.expect("No credentials found");
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
}
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
let client = get_artcraft_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let request = match draft_or_request {
VideoGenerationDraftOrRequest::Request(r) => r,
_ => panic!("expected Request variant (Artcraft skips draft)"),
};
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateVideoResponse::Artcraft(p) => {
println!("inference_job_token={:?}", p.inference_job_token);
println!("all_inference_job_tokens={:?}", p.all_inference_job_tokens);
}
other => println!("response: {:?}", other),
}
response
}
}
@@ -1,253 +0,0 @@
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
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, SupportedResolutions, UltraWideSupport,
};
use crate::generate::generate_video::providers::artcraft::seedance_2p0_u_fast::request::ArtcraftSeedance2p0UltraFastRequestState;
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_u_fast(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Seedance2p0UltraFast,
SupportedResolutions::Fast,
UltraWideSupport::Supported,
)?;
let state = ArtcraftSeedance2p0UltraFastRequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0UltraFast(state)))
}
#[cfg(test)]
mod tests {
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum;
use crate::api::router_bitrate::RouterBitrate;
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::character_list_ref::CharacterListRef;
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
use crate::api::router_provider::RouterProvider;
use crate::api::video_list_ref::VideoListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
use super::*;
// ── Field conversions ──
mod field_conversions {
use super::*;
#[test]
fn prompt_is_passed_through() {
let req = unwrap_request(artcraft_fast_builder_with(|b| { b.prompt = Some("test".to_string()); }));
assert_eq!(req.request.prompt, Some("test".to_string()));
}
#[test]
fn prompt_none_stays_none() {
let req = unwrap_request(artcraft_fast_builder_with(|_| {}));
assert!(req.request.prompt.is_none());
}
#[test]
fn model_is_seedance_2p0_fast() {
let req = unwrap_request(artcraft_fast_builder_with(|_| {}));
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0UltraFast)));
}
#[test]
fn duration_passed_through() {
let req = unwrap_request(artcraft_fast_builder_with(|b| { b.duration_seconds = Some(10); }));
assert_eq!(req.request.duration_seconds, Some(10));
}
#[test]
fn duration_clamped_to_max() {
let req = unwrap_request(artcraft_fast_builder_with(|b| { b.duration_seconds = Some(99); }));
assert_eq!(req.request.duration_seconds, Some(15));
}
#[test]
fn batch_count_passed_through() {
let req = unwrap_request(artcraft_fast_builder_with(|b| { b.video_batch_count = Some(4); }));
assert_eq!(req.request.video_batch_count, Some(4));
}
#[test]
fn bitrate_passed_through() {
let req = unwrap_request(artcraft_fast_builder_with(|b| { b.bitrate = Some(RouterBitrate::High); }));
assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High));
}
}
// ── Aspect ratio ──
mod aspect_ratio_tests {
use super::*;
#[test]
fn wide() {
let req = unwrap_request(artcraft_fast_builder_with(|b| {
b.aspect_ratio = Some(RouterAspectRatio::WideSixteenByNine);
}));
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::WideSixteenByNine));
}
#[test]
fn tall() {
let req = unwrap_request(artcraft_fast_builder_with(|b| {
b.aspect_ratio = Some(RouterAspectRatio::TallNineBySixteen);
}));
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::TallNineBySixteen));
}
#[test]
fn square() {
let req = unwrap_request(artcraft_fast_builder_with(|b| {
b.aspect_ratio = Some(RouterAspectRatio::Square);
}));
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::Square));
}
#[test]
fn none_stays_none() {
let req = unwrap_request(artcraft_fast_builder_with(|_| {}));
assert!(req.request.aspect_ratio.is_none());
}
}
// ── Resolution ──
mod resolution_tests {
use super::*;
#[test]
fn res_480p() {
let req = unwrap_request(artcraft_fast_builder_with(|b| {
b.resolution = Some(RouterResolution::FourEightyP);
}));
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
}
#[test]
fn res_720p() {
let req = unwrap_request(artcraft_fast_builder_with(|b| {
b.resolution = Some(RouterResolution::SevenTwentyP);
}));
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
}
#[test]
fn res_1080p_downgrades_to_720p() {
let req = unwrap_request(artcraft_fast_builder_with(|b| {
b.resolution = Some(RouterResolution::TenEightyP);
}));
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
}
#[test]
fn res_1080p_error_out() {
let result = build_artcraft_seedance_2p0_u_fast(GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0UltraFast,
provider: RouterProvider::Artcraft,
resolution: Some(RouterResolution::TenEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..Default::default()
});
assert!(result.is_err());
}
#[test]
fn none_stays_none() {
let req = unwrap_request(artcraft_fast_builder_with(|_| {}));
assert!(req.request.resolution.is_none());
}
}
// ── Media tokens ──
mod media_token_tests {
use super::*;
#[test]
fn start_frame_token_passed_through() {
let token = MediaFileToken::new("mf_start".to_string());
let req = unwrap_request(artcraft_fast_builder_with(|b| {
b.start_frame = Some(ImageRef::MediaFileToken(token.clone()));
}));
assert_eq!(req.request.start_frame_image_media_token, Some(token));
}
#[test]
fn url_start_frame_rejected() {
let result = build_artcraft_seedance_2p0_u_fast(GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0UltraFast,
provider: RouterProvider::Artcraft,
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
..Default::default()
});
assert!(result.is_err());
}
#[test]
fn reference_image_tokens_passed_through() {
let tokens = vec![
MediaFileToken::new("mf_a".to_string()),
MediaFileToken::new("mf_b".to_string()),
];
let req = unwrap_request(artcraft_fast_builder_with(|b| {
b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone()));
}));
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
}
#[test]
fn character_tokens_passed_through() {
let tokens = vec![
CharacterToken::new("char_a".to_string()),
CharacterToken::new("char_b".to_string()),
];
let req = unwrap_request(artcraft_fast_builder_with(|b| {
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone()));
}));
assert_eq!(req.request.reference_character_tokens, Some(tokens));
}
}
// ── Helpers ──
fn artcraft_fast_builder_with(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
let mut builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0UltraFast,
provider: RouterProvider::Artcraft,
duration_seconds: Some(5),
video_batch_count: Some(1),
..Default::default()
};
f(&mut builder);
builder
}
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p0UltraFastRequestState {
let result = build_artcraft_seedance_2p0_u_fast(builder).expect("build should succeed");
match result {
VideoGenerationDraftOrRequest::Request(
VideoGenerationRequest::ArtcraftSeedance2p0UltraFast(state)
) => state,
_ => panic!("expected ArtcraftSeedance2p0Fast request"),
}
}
}
@@ -1,168 +0,0 @@
use enums::common::generation::common_resolution::CommonResolution;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_u_fast::request::ArtcraftSeedance2p0UltraFastRequestState;
// ── Pricing constants ──
const CENTS_PER_SECOND_480P: f64 = 3.6267;
const CENTS_PER_SECOND_720P: f64 = 8.9089;
pub struct ArtcraftSeedance2p0UltraFastCostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
pub batch_count: u16,
}
impl ArtcraftSeedance2p0UltraFastCostState {
pub fn from_request(request: &ArtcraftSeedance2p0UltraFastRequestState) -> Self {
let resolution = request.request.resolution
.unwrap_or(CommonResolution::SevenTwentyP);
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
let batch_count = request.request.video_batch_count.unwrap_or(1);
Self { resolution, duration_seconds, batch_count }
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
let cents_per_second = match self.resolution {
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
// Fast model doesn't support 1080p; all non-480p resolves to 720p pricing
_ => CENTS_PER_SECOND_720P,
};
let cents_per_video = (cents_per_second * self.duration_seconds as f64).ceil() as u64;
let usd_cents = cents_per_video * self.batch_count as u64;
VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
#[cfg(test)]
mod tests {
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::router_provider::RouterProvider;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
mod pricing_720p {
use super::*;
#[test]
fn batch_1() {
// 8.9089 * 5 = 44.5445 → ceil = 45¢
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, 1), 45);
// 8.9089 * 10 = 89.089 → ceil = 90¢
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 10, 1), 90);
// 8.9089 * 15 = 133.6335 → ceil = 134¢
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 15, 1), 134);
}
#[test]
fn batch_2() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 10, 2), 180);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 10, 4), 360);
}
#[test]
fn none_defaults_to_720p() {
assert_eq!(cost_cents(None, 10, 1), cost_cents(Some(RouterResolution::SevenTwentyP), 10, 1));
}
}
mod pricing_480p {
use super::*;
#[test]
fn batch_1() {
// 3.6267 * 5 = 18.1335 → ceil = 19¢
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, 1), 19);
// 3.6267 * 10 = 36.267 → ceil = 37¢
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 10, 1), 37);
// 3.6267 * 15 = 54.4005 → ceil = 55¢
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 15, 1), 55);
}
#[test]
fn batch_4() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 10, 4), 148);
}
}
mod relative_pricing {
use super::*;
#[test]
fn cost_480p_cheaper_than_720p() {
let c480 = cost_cents(Some(RouterResolution::FourEightyP), 10, 1);
let c720 = cost_cents(Some(RouterResolution::SevenTwentyP), 10, 1);
assert!(c480 < c720, "480p ({c480}) should be < 720p ({c720})");
}
#[test]
fn cost_scales_with_duration() {
let c5 = cost_cents(None, 5, 1);
let c10 = cost_cents(None, 10, 1);
let c15 = cost_cents(None, 15, 1);
assert!(c5 < c10);
assert!(c10 < c15);
}
#[test]
fn fast_g_cheaper_than_standard_g() {
let fast = cost_cents(Some(RouterResolution::SevenTwentyP), 10, 1);
let standard = {
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Ultra,
provider: RouterProvider::Artcraft,
resolution: Some(RouterResolution::SevenTwentyP),
duration_seconds: Some(10),
video_batch_count: Some(1),
..Default::default()
};
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
};
assert!(fast < standard, "Fast ({fast}¢) should be < Standard ({standard}¢)");
}
#[test]
fn credits_equal_usd_cents() {
let cost = build_cost(None, 10, 1);
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
}
}
// -- Helpers --
fn build_cost(
resolution: Option<RouterResolution>,
duration_seconds: u16,
video_batch_count: u16,
) -> crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate {
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0UltraFast,
provider: RouterProvider::Artcraft,
resolution,
duration_seconds: Some(duration_seconds),
video_batch_count: Some(video_batch_count),
..Default::default()
};
builder.build2().expect("build2").estimate_cost().expect("estimate_cost")
}
fn cost_cents(resolution: Option<RouterResolution>, duration_seconds: u16, batch: u16) -> u64 {
build_cost(resolution, duration_seconds, batch).cost_in_usd_cents.unwrap()
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,227 +0,0 @@
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
use crate::generate::generate_video::providers::artcraft::request_common::send_artcraft_omni_video_request;
#[derive(Clone, Debug)]
pub struct ArtcraftSeedance2p0UltraFastRequestState {
pub request: OmniGenVideoCostAndGenerateRequest,
}
impl ArtcraftSeedance2p0UltraFastRequestState {
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
send_artcraft_omni_video_request(&self.request, client).await
}
}
#[cfg(test)]
mod tests {
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::character_list_ref::CharacterListRef;
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
use crate::api::router_provider::RouterProvider;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_client::RouterClient;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_client::utils::api_host::ApiHost;
use test_data::web::character_tokens::{JIM, KNIGHT};
use test_data::web::image_media_tokens::{
FOREST_BACKDROP_PRODUCTION_MEDIA_TOKEN,
JUNO_AT_LAKE_PRODUCTION_MEDIA_TOKEN,
WHITE_HOUSE_SUNSET_PRODUCTION_MEDIA_TOKEN,
};
// -- Aspect ratio tests --
mod aspect_ratio_tests {
use super::*;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn landscape() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A corgi running through a field of wildflowers at sunset.".to_string()),
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..artcraft_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn portrait() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A cat sitting on a windowsill watching rain.".to_string()),
aspect_ratio: Some(RouterAspectRatio::TallNineBySixteen),
..artcraft_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn square() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A hummingbird hovering near a flower.".to_string()),
aspect_ratio: Some(RouterAspectRatio::Square),
..artcraft_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
// -- Resolution tests --
mod resolution_tests {
use super::*;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn res_480p() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A shiba inu playing in autumn leaves.".to_string()),
resolution: Some(RouterResolution::FourEightyP),
..artcraft_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn res_720p() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A golden retriever catching a frisbee on the beach.".to_string()),
resolution: Some(RouterResolution::SevenTwentyP),
..artcraft_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
// -- Modality tests --
mod modality_tests {
use super::*;
use test_data::web::image_media_tokens::TALL_MOCHI_WITH_GLASSES_PRODUCTION_MEDIA_TOKEN;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn text_to_video() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A sky whale breaching in the open sky, pink clouds at dawn, cinematic.".to_string()),
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..artcraft_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn keyframe_start_and_end_frame() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("The dog walks from the lake to the forest.".to_string()),
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new(JUNO_AT_LAKE_PRODUCTION_MEDIA_TOKEN.to_string()))),
end_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new(FOREST_BACKDROP_PRODUCTION_MEDIA_TOKEN.to_string()))),
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..artcraft_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn image_references() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("The dog in @2 runs through the scenery in @1 towards the building in @3.".to_string()),
reference_images: Some(ImageListRef::MediaFileTokens(vec![
MediaFileToken::new(FOREST_BACKDROP_PRODUCTION_MEDIA_TOKEN.to_string()),
MediaFileToken::new(TALL_MOCHI_WITH_GLASSES_PRODUCTION_MEDIA_TOKEN.to_string()),
MediaFileToken::new(WHITE_HOUSE_SUNSET_PRODUCTION_MEDIA_TOKEN.to_string()),
])),
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..artcraft_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn character_references() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("@Jim and @Knight are sparring in a medieval arena.".to_string()),
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
CharacterToken::new(JIM.token.to_string()),
CharacterToken::new(KNIGHT.token.to_string()),
])),
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..artcraft_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
// -- Helpers --
fn artcraft_fast_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0UltraFast,
provider: RouterProvider::Artcraft,
duration_seconds: Some(4),
video_batch_count: Some(1),
..Default::default()
}
}
fn get_artcraft_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
.expect("Failed to read artcraft cookies");
let cookies = cookies.trim().to_string();
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
.expect("Failed to parse cookies")
.expect("No credentials found");
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
}
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
let client = get_artcraft_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let request = match draft_or_request {
VideoGenerationDraftOrRequest::Request(r) => r,
_ => panic!("expected Request variant (Artcraft skips draft)"),
};
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateVideoResponse::Artcraft(p) => {
println!("inference_job_token={:?}", p.inference_job_token);
println!("all_inference_job_tokens={:?}", p.all_inference_job_tokens);
}
other => println!("response: {:?}", other),
}
response
}
}
@@ -13,7 +13,7 @@ pub struct ArtcraftSeedance2p5CostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
pub has_video_references: bool,
pub total_input_seconds: u16,
pub maybe_total_input_seconds: Option<u16>,
}
impl ArtcraftSeedance2p5CostState {
@@ -30,20 +30,21 @@ impl ArtcraftSeedance2p5CostState {
resolution,
duration_seconds,
has_video_references,
total_input_seconds: request.total_input_seconds.unwrap_or(0),
maybe_total_input_seconds: request.total_input_seconds,
}
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Input-second clamping (the 4..=30 billing range) happens inside the
// shared pricing function.
let total_input_seconds = self.total_input_seconds;
// Input-second clamping (the 4..=30 billing range) and the
// unknown-or-zero-input failsafe happen inside the shared pricing
// function.
let maybe_total_input_seconds = self.maybe_total_input_seconds;
let usd_cents = seedance_2p5_usd_cents(
self.resolution,
self.duration_seconds,
self.has_video_references,
total_input_seconds,
maybe_total_input_seconds,
);
// ArtCraft credits: 100 credits = $1.00, so credits = cents.
@@ -115,10 +116,12 @@ mod tests {
}
#[test]
fn missing_input_seconds_bill_the_four_second_minimum() {
// No measured input still bills the 4-second total minimum:
// 7.24279835 ¢/s × (10+4) = 101.40 → 102¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 10, None), 102);
fn missing_or_zero_input_seconds_bill_the_thirty_second_maximum() {
// FAILSAFE: an unmeasured (or zero) input bills the 30-second MAXIMUM,
// matching the provider client's fallback:
// 7.24279835 ¢/s × (10+30) = 289.71 → 290¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 10, None), 290);
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 10, Some(0)), 290);
}
#[test]
@@ -14,7 +14,7 @@ pub struct ArtcraftSeedance2p5UltraCostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
pub has_video_references: bool,
pub total_input_seconds: u16,
pub maybe_total_input_seconds: Option<u16>,
}
impl ArtcraftSeedance2p5UltraCostState {
@@ -31,20 +31,21 @@ impl ArtcraftSeedance2p5UltraCostState {
resolution,
duration_seconds,
has_video_references,
total_input_seconds: request.total_input_seconds.unwrap_or(0),
maybe_total_input_seconds: request.total_input_seconds,
}
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Input-second clamping (the 4..=30 billing range) happens inside the
// shared pricing function.
let total_input_seconds = self.total_input_seconds;
// Input-second clamping (the 4..=30 billing range) and the
// unknown-or-zero-input failsafe happen inside the shared pricing
// function.
let maybe_total_input_seconds = self.maybe_total_input_seconds;
let usd_cents = seedance_2p5_ultra_usd_cents(
self.resolution,
self.duration_seconds,
self.has_video_references,
total_input_seconds,
maybe_total_input_seconds,
);
// ArtCraft credits: 100 credits = $1.00, so credits = cents.
@@ -133,10 +134,12 @@ mod tests {
}
#[test]
fn missing_input_seconds_bill_the_four_second_minimum() {
// No measured input still bills the 4-second total minimum:
// 8.55967078 ¢/s × (10+4) = 119.84 → 120¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 10, None), 120);
fn missing_or_zero_input_seconds_bill_the_thirty_second_maximum() {
// FAILSAFE: an unmeasured (or zero) input bills the 30-second MAXIMUM,
// matching the provider client's fallback:
// 8.55967078 ¢/s × (10+30) = 342.39 → 343¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 10, None), 343);
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 10, Some(0)), 343);
}
#[test]
@@ -11,30 +11,19 @@ use kinovi_web_client::generate::video::generate_seedance_2p5::{
// in hundredths of a USD cent per second so the math is exact integer arithmetic
// (no floating point), then rounded up to whole cents.
/// 4K output price, in hundredths of a USD cent per second (86.60 ¢/s).
const FOUR_K_CENTI_CENTS_PER_SECOND: u64 = 8660;
/// 4K output price, in hundredths of a USD cent per second (89.30 ¢/s).
const FOUR_K_CENTI_CENTS_PER_SECOND: u64 = 8930;
/// Extra hundredths of a USD cent per second when a reference video is attached
/// at 4K (17.20 ¢/s surcharge).
const FOUR_K_VIDEO_REFERENCE_SURCHARGE_CENTI_CENTS_PER_SECOND: u64 = 1720;
/// ArtCraft's price (USD cents) for Seedance 2.0 at 4K.
///
/// A reference video adds a per-second surcharge (4K with and without a video
/// reference are priced differently). The cost scales with duration and batch
/// count, rounded up to whole cents.
/// ArtCraft's price (USD cents) for Seedance 2.0 at 4K with no reference
/// videos attached. Each model prices reference videos through its own
/// with-reference rate card. The cost scales with duration and batch count,
/// rounded up to whole cents.
pub fn seedance_2p0_four_k_usd_cents(
duration_seconds: u16,
batch_count: u16,
has_video_reference: bool,
) -> u64 {
let mut centi_cents_per_second = FOUR_K_CENTI_CENTS_PER_SECOND;
if has_video_reference {
centi_cents_per_second += FOUR_K_VIDEO_REFERENCE_SURCHARGE_CENTI_CENTS_PER_SECOND;
}
let total_centi_cents =
centi_cents_per_second * duration_seconds as u64 * batch_count as u64;
FOUR_K_CENTI_CENTS_PER_SECOND * duration_seconds as u64 * batch_count as u64;
// Round up to whole cents.
total_centi_cents.div_ceil(100)
@@ -51,22 +40,22 @@ pub fn seedance_2p0_four_k_usd_cents(
// rate sets and helpers below.
/// Regular Mini — 480p price, USD cents per second.
const MINI_CENTS_PER_SECOND_480P: f64 = 3.24074074;
const MINI_CENTS_PER_SECOND_480P: f64 = 3.45;
/// Regular Mini — 480p reference-video surcharge, USD cents per second.
const MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_480P: f64 = 0.86419753;
const MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_480P: f64 = 0.90;
/// Regular Mini — 720p price, USD cents per second.
const MINI_CENTS_PER_SECOND_720P: f64 = 8.64197531;
const MINI_CENTS_PER_SECOND_720P: f64 = 8.90;
/// Regular Mini — 720p reference-video surcharge, USD cents per second.
const MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_720P: f64 = 1.72839506;
const MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_720P: f64 = 1.80;
/// BytePlus / BytePlus Ultra Mini — 480p price, USD cents per second.
const BYTEPLUS_MINI_CENTS_PER_SECOND_480P: f64 = 3.27160494;
const BYTEPLUS_MINI_CENTS_PER_SECOND_480P: f64 = 3.55;
/// BytePlus / BytePlus Ultra Mini — 480p reference-video surcharge, USD cents per second.
const BYTEPLUS_MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_480P: f64 = 0.87242798;
const BYTEPLUS_MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_480P: f64 = 0.95;
/// BytePlus / BytePlus Ultra Mini — 720p price, USD cents per second.
const BYTEPLUS_MINI_CENTS_PER_SECOND_720P: f64 = 8.72427984;
const BYTEPLUS_MINI_CENTS_PER_SECOND_720P: f64 = 9.10;
/// BytePlus / BytePlus Ultra Mini — 720p reference-video surcharge, USD cents per second.
const BYTEPLUS_MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_720P: f64 = 1.74485597;
const BYTEPLUS_MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_720P: f64 = 1.85;
/// ArtCraft's price (USD cents) for the regular Seedance 2.0 Mini.
///
@@ -165,7 +154,7 @@ pub fn seedance_2p5_usd_cents(
resolution: CommonResolution,
duration_seconds: u16,
has_video_references: bool,
total_input_seconds: u16,
maybe_total_input_seconds: Option<u16>,
) -> u64 {
let (cents_per_second, billed_seconds) = if has_video_references {
let rate = match resolution {
@@ -175,8 +164,18 @@ pub fn seedance_2p5_usd_cents(
};
// The TOTAL input duration clamps to the 4..=30 second billing range
// (three 1s videos sum to 3 and bill 4; three 3s videos bill 9).
let billed_input_seconds = total_input_seconds
.clamp(u16::from(MIN_BILLED_INPUT_SECONDS), u16::from(MAX_BILLED_INPUT_SECONDS));
//
// FAILSAFE: an unknown (never probed) or zero input duration bills the
// 30-second MAXIMUM, matching the provider client's own fallback. It
// must never default toward the minimum — billing 4 input seconds for
// an unmeasured input while the provider assumes 30 sells input seconds
// far below cost.
let billed_input_seconds = match maybe_total_input_seconds {
None | Some(0) => u16::from(MAX_BILLED_INPUT_SECONDS),
Some(seconds) => {
seconds.clamp(u16::from(MIN_BILLED_INPUT_SECONDS), u16::from(MAX_BILLED_INPUT_SECONDS))
}
};
(rate, u64::from(duration_seconds) + u64::from(billed_input_seconds))
} else {
let rate = match resolution {
@@ -209,7 +208,7 @@ pub fn seedance_2p5_ultra_usd_cents(
resolution: CommonResolution,
duration_seconds: u16,
has_video_references: bool,
total_input_seconds: u16,
maybe_total_input_seconds: Option<u16>,
) -> u64 {
let (cents_per_second, billed_seconds) = if has_video_references {
let rate = match resolution {
@@ -219,8 +218,18 @@ pub fn seedance_2p5_ultra_usd_cents(
};
// The TOTAL input duration clamps to the 4..=30 second billing range
// (three 1s videos sum to 3 and bill 4; three 3s videos bill 9).
let billed_input_seconds = total_input_seconds
.clamp(u16::from(MIN_BILLED_INPUT_SECONDS), u16::from(MAX_BILLED_INPUT_SECONDS));
//
// FAILSAFE: an unknown (never probed) or zero input duration bills the
// 30-second MAXIMUM, matching the provider client's own fallback. It
// must never default toward the minimum — billing 4 input seconds for
// an unmeasured input while the provider assumes 30 sells input seconds
// far below cost.
let billed_input_seconds = match maybe_total_input_seconds {
None | Some(0) => u16::from(MAX_BILLED_INPUT_SECONDS),
Some(seconds) => {
seconds.clamp(u16::from(MIN_BILLED_INPUT_SECONDS), u16::from(MAX_BILLED_INPUT_SECONDS))
}
};
(rate, u64::from(duration_seconds) + u64::from(billed_input_seconds))
} else {
let rate = match resolution {
@@ -263,37 +272,17 @@ mod tests {
use super::*;
#[test]
fn four_k_without_video_reference() {
assert_eq!(seedance_2p0_four_k_usd_cents(4, 1, false), 347);
assert_eq!(seedance_2p0_four_k_usd_cents(5, 1, false), 433);
assert_eq!(seedance_2p0_four_k_usd_cents(10, 1, false), 866);
assert_eq!(seedance_2p0_four_k_usd_cents(15, 1, false), 1299);
}
#[test]
fn four_k_with_video_reference() {
assert_eq!(seedance_2p0_four_k_usd_cents(4, 1, true), 416);
assert_eq!(seedance_2p0_four_k_usd_cents(5, 1, true), 519);
assert_eq!(seedance_2p0_four_k_usd_cents(10, 1, true), 1038);
assert_eq!(seedance_2p0_four_k_usd_cents(15, 1, true), 1557);
}
#[test]
fn video_reference_always_costs_more() {
for &duration in &[4u16, 5, 10, 15] {
assert!(
seedance_2p0_four_k_usd_cents(duration, 1, true)
> seedance_2p0_four_k_usd_cents(duration, 1, false),
"video reference should cost more at {duration}s",
);
}
fn four_k_base_prices() {
assert_eq!(seedance_2p0_four_k_usd_cents(4, 1), 358);
assert_eq!(seedance_2p0_four_k_usd_cents(5, 1), 447);
assert_eq!(seedance_2p0_four_k_usd_cents(10, 1), 893);
assert_eq!(seedance_2p0_four_k_usd_cents(15, 1), 1340);
}
#[test]
fn batch_count_multiplies() {
assert_eq!(seedance_2p0_four_k_usd_cents(5, 2, false), 866);
assert_eq!(seedance_2p0_four_k_usd_cents(5, 4, false), 1732);
assert_eq!(seedance_2p0_four_k_usd_cents(5, 2, true), 1038);
assert_eq!(seedance_2p0_four_k_usd_cents(5, 2), 893);
assert_eq!(seedance_2p0_four_k_usd_cents(5, 4), 1786);
}
// ── Seedance 2.0 Mini ──
@@ -310,34 +299,34 @@ mod tests {
#[test]
fn four_eighty_p_without_video_reference() {
assert_eq!(cents(CommonResolution::FourEightyP, 4, false), 13);
assert_eq!(cents(CommonResolution::FourEightyP, 5, false), 17);
assert_eq!(cents(CommonResolution::FourEightyP, 10, false), 33);
assert_eq!(cents(CommonResolution::FourEightyP, 15, false), 49);
assert_eq!(cents(CommonResolution::FourEightyP, 4, false), 14);
assert_eq!(cents(CommonResolution::FourEightyP, 5, false), 18);
assert_eq!(cents(CommonResolution::FourEightyP, 10, false), 35);
assert_eq!(cents(CommonResolution::FourEightyP, 15, false), 52);
}
#[test]
fn four_eighty_p_with_video_reference() {
assert_eq!(cents(CommonResolution::FourEightyP, 4, true), 17);
assert_eq!(cents(CommonResolution::FourEightyP, 5, true), 21);
assert_eq!(cents(CommonResolution::FourEightyP, 10, true), 42);
assert_eq!(cents(CommonResolution::FourEightyP, 15, true), 62);
assert_eq!(cents(CommonResolution::FourEightyP, 4, true), 18);
assert_eq!(cents(CommonResolution::FourEightyP, 5, true), 22);
assert_eq!(cents(CommonResolution::FourEightyP, 10, true), 44);
assert_eq!(cents(CommonResolution::FourEightyP, 15, true), 66);
}
#[test]
fn seven_twenty_p_without_video_reference() {
assert_eq!(cents(CommonResolution::SevenTwentyP, 4, false), 35);
assert_eq!(cents(CommonResolution::SevenTwentyP, 5, false), 44);
assert_eq!(cents(CommonResolution::SevenTwentyP, 10, false), 87);
assert_eq!(cents(CommonResolution::SevenTwentyP, 15, false), 130);
assert_eq!(cents(CommonResolution::SevenTwentyP, 4, false), 36);
assert_eq!(cents(CommonResolution::SevenTwentyP, 5, false), 45);
assert_eq!(cents(CommonResolution::SevenTwentyP, 10, false), 89);
assert_eq!(cents(CommonResolution::SevenTwentyP, 15, false), 134);
}
#[test]
fn seven_twenty_p_with_video_reference() {
assert_eq!(cents(CommonResolution::SevenTwentyP, 4, true), 42);
assert_eq!(cents(CommonResolution::SevenTwentyP, 5, true), 52);
assert_eq!(cents(CommonResolution::SevenTwentyP, 10, true), 104);
assert_eq!(cents(CommonResolution::SevenTwentyP, 15, true), 156);
assert_eq!(cents(CommonResolution::SevenTwentyP, 4, true), 43);
assert_eq!(cents(CommonResolution::SevenTwentyP, 5, true), 54);
assert_eq!(cents(CommonResolution::SevenTwentyP, 10, true), 108);
assert_eq!(cents(CommonResolution::SevenTwentyP, 15, true), 161);
}
#[test]
@@ -364,9 +353,9 @@ mod tests {
fn batch_count_scales_the_total() {
// Batch is baked in before the single round-up, so batched totals can be
// a cent under N× the single price.
assert_eq!(seedance_2p0_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 2, false), 87);
assert_eq!(seedance_2p0_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 4, false), 173);
assert_eq!(seedance_2p0_mini_usd_cents(CommonResolution::FourEightyP, 5, 2, false), 33);
assert_eq!(seedance_2p0_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 2, false), 89);
assert_eq!(seedance_2p0_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 4, false), 178);
assert_eq!(seedance_2p0_mini_usd_cents(CommonResolution::FourEightyP, 5, 2, false), 35);
}
}
@@ -384,34 +373,34 @@ mod tests {
#[test]
fn four_eighty_p_without_video_reference() {
assert_eq!(cents(CommonResolution::FourEightyP, 4, false), 14);
assert_eq!(cents(CommonResolution::FourEightyP, 5, false), 17);
assert_eq!(cents(CommonResolution::FourEightyP, 10, false), 33);
assert_eq!(cents(CommonResolution::FourEightyP, 15, false), 50);
assert_eq!(cents(CommonResolution::FourEightyP, 4, false), 15);
assert_eq!(cents(CommonResolution::FourEightyP, 5, false), 18);
assert_eq!(cents(CommonResolution::FourEightyP, 10, false), 36);
assert_eq!(cents(CommonResolution::FourEightyP, 15, false), 54);
}
#[test]
fn four_eighty_p_with_video_reference() {
assert_eq!(cents(CommonResolution::FourEightyP, 4, true), 17);
assert_eq!(cents(CommonResolution::FourEightyP, 5, true), 21);
assert_eq!(cents(CommonResolution::FourEightyP, 10, true), 42);
assert_eq!(cents(CommonResolution::FourEightyP, 15, true), 63);
assert_eq!(cents(CommonResolution::FourEightyP, 4, true), 18);
assert_eq!(cents(CommonResolution::FourEightyP, 5, true), 23);
assert_eq!(cents(CommonResolution::FourEightyP, 10, true), 45);
assert_eq!(cents(CommonResolution::FourEightyP, 15, true), 68);
}
#[test]
fn seven_twenty_p_without_video_reference() {
assert_eq!(cents(CommonResolution::SevenTwentyP, 4, false), 35);
assert_eq!(cents(CommonResolution::SevenTwentyP, 5, false), 44);
assert_eq!(cents(CommonResolution::SevenTwentyP, 10, false), 88);
assert_eq!(cents(CommonResolution::SevenTwentyP, 15, false), 131);
assert_eq!(cents(CommonResolution::SevenTwentyP, 4, false), 37);
assert_eq!(cents(CommonResolution::SevenTwentyP, 5, false), 46);
assert_eq!(cents(CommonResolution::SevenTwentyP, 10, false), 91);
assert_eq!(cents(CommonResolution::SevenTwentyP, 15, false), 137);
}
#[test]
fn seven_twenty_p_with_video_reference() {
assert_eq!(cents(CommonResolution::SevenTwentyP, 4, true), 42);
assert_eq!(cents(CommonResolution::SevenTwentyP, 5, true), 53);
assert_eq!(cents(CommonResolution::SevenTwentyP, 10, true), 105);
assert_eq!(cents(CommonResolution::SevenTwentyP, 15, true), 158);
assert_eq!(cents(CommonResolution::SevenTwentyP, 4, true), 44);
assert_eq!(cents(CommonResolution::SevenTwentyP, 5, true), 55);
assert_eq!(cents(CommonResolution::SevenTwentyP, 10, true), 110);
assert_eq!(cents(CommonResolution::SevenTwentyP, 15, true), 165);
}
#[test]
@@ -439,9 +428,9 @@ mod tests {
#[test]
fn batch_count_scales_the_total() {
assert_eq!(seedance_2p0_byteplus_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 2, false), 88);
assert_eq!(seedance_2p0_byteplus_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 4, false), 175);
assert_eq!(seedance_2p0_byteplus_mini_usd_cents(CommonResolution::FourEightyP, 5, 2, false), 33);
assert_eq!(seedance_2p0_byteplus_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 2, false), 91);
assert_eq!(seedance_2p0_byteplus_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 4, false), 182);
assert_eq!(seedance_2p0_byteplus_mini_usd_cents(CommonResolution::FourEightyP, 5, 2, false), 36);
}
}
}
@@ -1,2 +0,0 @@
pub mod seedance_2p0_g;
pub mod seedance_2p0_fast_g;
@@ -1,323 +0,0 @@
use gmicloud_client::requests::api::video::seedance_2_0_fast_260128::api::{
Seedance20FastRatio, Seedance20FastRequest, Seedance20FastResolution,
};
use crate::api::audio_list_ref::AudioListRef;
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_resolution::RouterResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
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::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0UltraFastRequestState;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
pub fn build_gmicloud_seedance_2p0_u_fast(
mut builder: GenerateVideoRequestBuilder,
) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
let strategy = builder.request_mismatch_mitigation_strategy;
let ratio = plan_ratio(builder.aspect_ratio.take(), strategy)?;
let resolution = plan_resolution(builder.resolution.take(), strategy)?;
let duration = builder.duration_seconds.take().map(|d| (d as u8).clamp(4, 15));
let prompt = builder.prompt.take().unwrap_or_default();
let first_frame = resolve_url(builder.start_frame.take())?;
let last_frame = resolve_url(builder.end_frame.take())?;
let reference_images = resolve_url_list_from_images(builder.reference_images.take())?;
let reference_videos = resolve_url_list_from_videos(builder.reference_videos.take())?;
let reference_audios = resolve_url_list_from_audios(builder.reference_audio.take())?;
let request = Seedance20FastRequest {
prompt,
duration,
resolution,
ratio,
seed: None,
watermark: Some(false),
generate_audio: Some(true),
web_search: None,
first_frame,
last_frame,
reference_images,
reference_videos,
reference_audios,
reference_asset_ids: None,
};
let state = GmiCloudSeedance2p0UltraFastRequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0UltraFast(state)))
}
fn plan_ratio(
aspect_ratio: Option<RouterAspectRatio>,
_strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<Seedance20FastRatio>, ArtcraftRouterError> {
match aspect_ratio {
None | Some(RouterAspectRatio::Auto) | Some(RouterAspectRatio::Auto2k)
| Some(RouterAspectRatio::Auto3k) | Some(RouterAspectRatio::Auto4k) => Ok(None),
Some(RouterAspectRatio::WideSixteenByNine) | Some(RouterAspectRatio::Wide) => Ok(Some(Seedance20FastRatio::Landscape16x9)),
Some(RouterAspectRatio::TallNineBySixteen) | Some(RouterAspectRatio::Tall) => Ok(Some(Seedance20FastRatio::Portrait9x16)),
Some(RouterAspectRatio::Square) | Some(RouterAspectRatio::SquareHd) => Ok(Some(Seedance20FastRatio::Square)),
Some(RouterAspectRatio::WideFourByThree) => Ok(Some(Seedance20FastRatio::Standard4x3)),
Some(RouterAspectRatio::TallThreeByFour) => Ok(Some(Seedance20FastRatio::Portrait3x4)),
Some(RouterAspectRatio::WideTwentyOneByNine) | Some(RouterAspectRatio::TallNineByTwentyOne) => {
Ok(Some(Seedance20FastRatio::UltraWide21x9))
}
Some(RouterAspectRatio::WideThreeByTwo) | Some(RouterAspectRatio::WideFiveByFour)
| Some(RouterAspectRatio::TallFourByFive) | Some(RouterAspectRatio::TallTwoByThree) => {
Ok(Some(Seedance20FastRatio::Adaptive))
}
}
}
fn plan_resolution(
resolution: Option<RouterResolution>,
_strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<Seedance20FastResolution>, ArtcraftRouterError> {
match resolution {
None => Ok(None),
Some(RouterResolution::FourEightyP) => Ok(Some(Seedance20FastResolution::FourEightyP)),
Some(RouterResolution::SevenTwentyP) => Ok(Some(Seedance20FastResolution::SevenTwentyP)),
// Fast model doesn't support 1080p — fall back to 720p
Some(RouterResolution::TenEightyP) => Ok(Some(Seedance20FastResolution::SevenTwentyP)),
Some(RouterResolution::HalfK) | Some(RouterResolution::OneK) => Ok(Some(Seedance20FastResolution::FourEightyP)),
Some(RouterResolution::TwoK) | Some(RouterResolution::ThreeK) | Some(RouterResolution::FourK) => {
Ok(Some(Seedance20FastResolution::SevenTwentyP))
}
}
}
fn resolve_url(image_ref: Option<ImageRef>) -> Result<Option<String>, ArtcraftRouterError> {
match image_ref {
None => Ok(None),
Some(ImageRef::Url(url)) => Ok(Some(url)),
Some(ImageRef::MediaFileToken(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "start_frame/end_frame",
value: "GmiCloud only supports image URLs, not media file tokens".to_string(),
}))
}
}
}
fn resolve_url_list_from_images(
list_ref: Option<ImageListRef>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
match list_ref {
None => Ok(None),
Some(ImageListRef::Urls(urls)) => Ok(Some(urls)),
Some(ImageListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "reference_images",
value: "GmiCloud only supports image URLs, not media file tokens".to_string(),
}))
}
}
}
fn resolve_url_list_from_videos(
list_ref: Option<VideoListRef>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
match list_ref {
None => Ok(None),
Some(VideoListRef::Urls(urls)) => Ok(Some(urls)),
Some(VideoListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "reference_videos",
value: "GmiCloud only supports video URLs, not media file tokens".to_string(),
}))
}
}
}
#[cfg(test)]
mod tests {
use gmicloud_client::requests::api::video::seedance_2_0_fast_260128::api::{
Seedance20FastRatio, Seedance20FastResolution,
};
use tokens::tokens::media_files::MediaFileToken;
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
use crate::api::router_provider::RouterProvider;
use crate::api::video_list_ref::VideoListRef;
use crate::api::audio_list_ref::AudioListRef;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
use super::*;
mod field_conversions {
use super::*;
#[test]
fn prompt_passed_through() {
let req = unwrap_request(make_builder(|b| { b.prompt = Some("fast test".to_string()); }));
assert_eq!(req.request.prompt, "fast test");
}
#[test]
fn duration_passed_through() {
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(8); }));
assert_eq!(req.request.duration, Some(8));
}
#[test]
fn duration_clamped_to_min() {
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(2); }));
assert_eq!(req.request.duration, Some(4));
}
#[test]
fn duration_clamped_to_max() {
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(30); }));
assert_eq!(req.request.duration, Some(15));
}
}
mod resolution_tests {
use super::*;
#[test]
fn res_480p() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourEightyP); }));
assert_eq!(req.request.resolution, Some(Seedance20FastResolution::FourEightyP));
}
#[test]
fn res_720p() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::SevenTwentyP); }));
assert_eq!(req.request.resolution, Some(Seedance20FastResolution::SevenTwentyP));
}
#[test]
fn res_1080p_falls_back_to_720p() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::TenEightyP); }));
assert_eq!(req.request.resolution, Some(Seedance20FastResolution::SevenTwentyP));
}
#[test]
fn none_stays_none() {
let req = unwrap_request(make_builder(|_| {}));
assert!(req.request.resolution.is_none());
}
}
mod aspect_ratio_tests {
use super::*;
#[test]
fn landscape_16x9() {
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::WideSixteenByNine); }));
assert_eq!(req.request.ratio, Some(Seedance20FastRatio::Landscape16x9));
}
#[test]
fn square() {
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::Square); }));
assert_eq!(req.request.ratio, Some(Seedance20FastRatio::Square));
}
#[test]
fn auto_maps_to_none() {
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::Auto); }));
assert_eq!(req.request.ratio, None);
}
}
mod media_url_tests {
use super::*;
#[test]
fn start_frame_url_passed_through() {
let req = unwrap_request(make_builder(|b| {
b.start_frame = Some(ImageRef::Url("https://example.com/img.png".to_string()));
}));
assert_eq!(req.request.first_frame, Some("https://example.com/img.png".to_string()));
}
#[test]
fn media_file_token_rejected() {
let result = build_gmicloud_seedance_2p0_u_fast(GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_x".to_string()))),
..base_builder()
});
assert!(result.is_err());
}
#[test]
fn reference_image_urls_passed_through() {
let urls = vec!["https://example.com/ref.png".to_string()];
let req = unwrap_request(make_builder(|b| {
b.reference_images = Some(ImageListRef::Urls(urls.clone()));
}));
assert_eq!(req.request.reference_images, Some(urls));
}
#[test]
fn reference_video_urls_passed_through() {
let urls = vec!["https://example.com/ref.mp4".to_string()];
let req = unwrap_request(make_builder(|b| {
b.reference_videos = Some(VideoListRef::Urls(urls.clone()));
}));
assert_eq!(req.request.reference_videos, Some(urls));
}
#[test]
fn reference_audio_urls_passed_through() {
let urls = vec!["https://example.com/ref.wav".to_string()];
let req = unwrap_request(make_builder(|b| {
b.reference_audio = Some(AudioListRef::Urls(urls.clone()));
}));
assert_eq!(req.request.reference_audios, Some(urls));
}
}
fn base_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0UltraFast,
provider: RouterProvider::GmiCloud,
duration_seconds: Some(5),
video_batch_count: Some(1),
..Default::default()
}
}
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
let mut builder = base_builder();
f(&mut builder);
builder
}
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> GmiCloudSeedance2p0UltraFastRequestState {
let result = build_gmicloud_seedance_2p0_u_fast(builder).expect("build should succeed");
match result {
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0UltraFast(state)) => state,
_ => panic!("expected GmiCloudSeedance2p0FastG request"),
}
}
}
fn resolve_url_list_from_audios(
list_ref: Option<AudioListRef>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
match list_ref {
None => Ok(None),
Some(AudioListRef::Urls(urls)) => Ok(Some(urls)),
Some(AudioListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "reference_audios",
value: "GmiCloud only supports audio URLs, not media file tokens".to_string(),
}))
}
}
}
@@ -1,125 +0,0 @@
use gmicloud_client::traits::gmicloud_request_cost_calculator_trait::GmiCloudRequestCostCalculator;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0UltraFastRequestState;
pub struct GmiCloudSeedance2p0UltraFastCostState {
request: GmiCloudSeedance2p0UltraFastRequestState,
}
impl GmiCloudSeedance2p0UltraFastCostState {
pub fn from_request(request: &GmiCloudSeedance2p0UltraFastRequestState) -> Self {
Self { request: request.clone() }
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
let cost_in_usd_cents = self.request.request.calculate_cost_in_cents();
VideoGenerationCostEstimate {
cost_in_credits: None,
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
#[cfg(test)]
mod tests {
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::router_provider::RouterProvider;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
mod pricing_720p {
use super::*;
#[test]
fn default_5s() {
// 720p: 3.1 ¢/s * 5 = 15.5 → ceil = 16¢
assert_eq!(cost_cents(None, 5), 16);
}
#[test]
fn default_10s() {
// 3.1 * 10 = 31¢
assert_eq!(cost_cents(None, 10), 31);
}
#[test]
fn default_15s() {
// 3.1 * 15 = 46.5 → ceil = 47¢
assert_eq!(cost_cents(None, 15), 47);
}
}
mod pricing_480p {
use super::*;
#[test]
fn p480_5s() {
// 1.0 * 5 = 5¢
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5), 5);
}
#[test]
fn p480_10s() {
// 1.0 * 10 = 10¢
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 10), 10);
}
#[test]
fn p480_15s() {
// 1.0 * 15 = 15¢
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 15), 15);
}
}
mod relative_pricing {
use super::*;
#[test]
fn higher_resolution_costs_more() {
let c480 = cost_cents(Some(RouterResolution::FourEightyP), 10);
let c720 = cost_cents(Some(RouterResolution::SevenTwentyP), 10);
assert!(c480 < c720, "480p ({c480}) should be < 720p ({c720})");
}
#[test]
fn fast_cheaper_than_standard() {
let fast = cost_cents(Some(RouterResolution::SevenTwentyP), 10);
let standard = {
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Ultra,
provider: RouterProvider::GmiCloud,
resolution: Some(RouterResolution::SevenTwentyP),
duration_seconds: Some(10),
video_batch_count: Some(1),
..Default::default()
};
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
};
assert!(fast < standard, "Fast ({fast}¢) should be < Standard ({standard}¢)");
}
}
fn cost_cents(resolution: Option<RouterResolution>, duration_seconds: u16) -> u64 {
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0UltraFast,
provider: RouterProvider::GmiCloud,
resolution,
duration_seconds: Some(duration_seconds),
video_batch_count: Some(1),
..Default::default()
};
builder.build2()
.expect("build2 should succeed")
.estimate_cost()
.expect("estimate_cost should succeed")
.cost_in_usd_cents
.unwrap()
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,100 +0,0 @@
use gmicloud_client::requests::api::video::seedance_2_0_fast_260128::api::Seedance20FastRequest;
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_video::generate_video_response::{
GenerateVideoResponse, GmiCloudVideoResponsePayload,
};
#[derive(Clone, Debug)]
pub struct GmiCloudSeedance2p0UltraFastRequestState {
pub request: Seedance20FastRequest,
}
impl GmiCloudSeedance2p0UltraFastRequestState {
pub async fn send(&self, client: &RouterGmiCloudClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let response = self.request.send_request(&client.api_key)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::GmiCloud(err)))?;
Ok(GenerateVideoResponse::GmiCloud(GmiCloudVideoResponsePayload {
request_id: response.request_id,
}))
}
}
#[cfg(test)]
mod tests {
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::image_ref::ImageRef;
use crate::api::router_provider::RouterProvider;
use crate::client::router_client::RouterClient;
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use gmicloud_client::creds::gmicloud_api_key::GmiCloudApiKey;
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
#[tokio::test]
#[ignore] // requires real API key, incurs costs
async fn test_text_to_video_720p() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A shiba inu playing in autumn leaves.".to_string()),
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
resolution: Some(RouterResolution::SevenTwentyP),
duration_seconds: Some(5),
..gmicloud_fast_builder()
}).await;
println!("response: {:?}", response);
assert!(matches!(response, GenerateVideoResponse::GmiCloud(_)));
}
#[tokio::test]
#[ignore] // requires real API key, incurs costs
async fn test_image_to_video_480p() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("The dog starts running and splashing in the lake.".to_string()),
start_frame: Some(ImageRef::Url(JUNO_AT_LAKE_IMAGE_URL.to_string())),
resolution: Some(RouterResolution::FourEightyP),
duration_seconds: Some(5),
..gmicloud_fast_builder()
}).await;
println!("response: {:?}", response);
assert!(matches!(response, GenerateVideoResponse::GmiCloud(_)));
}
fn gmicloud_fast_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0UltraFast,
provider: RouterProvider::GmiCloud,
video_batch_count: Some(1),
..Default::default()
}
}
fn get_gmicloud_client() -> RouterClient {
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/gmicloud_api_key.txt")
.expect("Failed to read GmiCloud API key");
let api_key = GmiCloudApiKey::from_str(&secret);
RouterClient::GmiCloud(RouterGmiCloudClient::new(api_key))
}
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
let client = get_gmicloud_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let request = match draft_or_request {
VideoGenerationDraftOrRequest::Request(r) => r,
_ => panic!("expected Request variant (GmiCloud skips draft)"),
};
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateVideoResponse::GmiCloud(p) => println!("request_id={}", p.request_id),
other => println!("response: {:?}", other),
}
response
}
}
@@ -1,355 +0,0 @@
use gmicloud_client::requests::api::video::seedance_2_0_260128::api::{
Seedance20Ratio, Seedance20Request, Seedance20Resolution,
};
use crate::api::audio_list_ref::AudioListRef;
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_resolution::RouterResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
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::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0UltraRequestState;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
pub fn build_gmicloud_seedance_2p0_u(
mut builder: GenerateVideoRequestBuilder,
) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
let strategy = builder.request_mismatch_mitigation_strategy;
let ratio = plan_ratio(builder.aspect_ratio.take(), strategy)?;
let resolution = plan_resolution(builder.resolution.take(), strategy)?;
let duration = builder.duration_seconds.take().map(|d| (d as u8).clamp(4, 15));
let prompt = builder.prompt.take().unwrap_or_default();
let first_frame = resolve_url(builder.start_frame.take())?;
let last_frame = resolve_url(builder.end_frame.take())?;
let reference_images = resolve_url_list_from_images(builder.reference_images.take())?;
let reference_videos = resolve_url_list_from_videos(builder.reference_videos.take())?;
let reference_audios = resolve_url_list_from_audios(builder.reference_audio.take())?;
let request = Seedance20Request {
prompt,
duration,
resolution,
ratio,
seed: None,
watermark: Some(false),
generate_audio: Some(true),
web_search: None,
first_frame,
last_frame,
reference_images,
reference_videos,
reference_audios,
reference_asset_ids: None,
};
let state = GmiCloudSeedance2p0UltraRequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0Ultra(state)))
}
fn plan_ratio(
aspect_ratio: Option<RouterAspectRatio>,
_strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<Seedance20Ratio>, ArtcraftRouterError> {
match aspect_ratio {
None | Some(RouterAspectRatio::Auto) | Some(RouterAspectRatio::Auto2k)
| Some(RouterAspectRatio::Auto3k) | Some(RouterAspectRatio::Auto4k) => Ok(None),
Some(RouterAspectRatio::WideSixteenByNine) | Some(RouterAspectRatio::Wide) => Ok(Some(Seedance20Ratio::Landscape16x9)),
Some(RouterAspectRatio::TallNineBySixteen) | Some(RouterAspectRatio::Tall) => Ok(Some(Seedance20Ratio::Portrait9x16)),
Some(RouterAspectRatio::Square) | Some(RouterAspectRatio::SquareHd) => Ok(Some(Seedance20Ratio::Square)),
Some(RouterAspectRatio::WideFourByThree) => Ok(Some(Seedance20Ratio::Standard4x3)),
Some(RouterAspectRatio::TallThreeByFour) => Ok(Some(Seedance20Ratio::Portrait3x4)),
Some(RouterAspectRatio::WideTwentyOneByNine) | Some(RouterAspectRatio::TallNineByTwentyOne) => {
Ok(Some(Seedance20Ratio::UltraWide21x9))
}
Some(RouterAspectRatio::WideThreeByTwo) | Some(RouterAspectRatio::WideFiveByFour)
| Some(RouterAspectRatio::TallFourByFive) | Some(RouterAspectRatio::TallTwoByThree) => {
Ok(Some(Seedance20Ratio::Adaptive))
}
}
}
fn plan_resolution(
resolution: Option<RouterResolution>,
_strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<Seedance20Resolution>, ArtcraftRouterError> {
match resolution {
None => Ok(None),
Some(RouterResolution::FourEightyP) => Ok(Some(Seedance20Resolution::FourEightyP)),
Some(RouterResolution::SevenTwentyP) => Ok(Some(Seedance20Resolution::SevenTwentyP)),
Some(RouterResolution::TenEightyP) => Ok(Some(Seedance20Resolution::TenEightyP)),
Some(RouterResolution::HalfK) | Some(RouterResolution::OneK) => Ok(Some(Seedance20Resolution::FourEightyP)),
Some(RouterResolution::TwoK) | Some(RouterResolution::ThreeK) | Some(RouterResolution::FourK) => {
Ok(Some(Seedance20Resolution::TenEightyP))
}
}
}
fn resolve_url(image_ref: Option<ImageRef>) -> Result<Option<String>, ArtcraftRouterError> {
match image_ref {
None => Ok(None),
Some(ImageRef::Url(url)) => Ok(Some(url)),
Some(ImageRef::MediaFileToken(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "start_frame/end_frame",
value: "GmiCloud only supports image URLs, not media file tokens".to_string(),
}))
}
}
}
fn resolve_url_list_from_images(
list_ref: Option<ImageListRef>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
match list_ref {
None => Ok(None),
Some(ImageListRef::Urls(urls)) => Ok(Some(urls)),
Some(ImageListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "reference_images",
value: "GmiCloud only supports image URLs, not media file tokens".to_string(),
}))
}
}
}
fn resolve_url_list_from_videos(
list_ref: Option<VideoListRef>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
match list_ref {
None => Ok(None),
Some(VideoListRef::Urls(urls)) => Ok(Some(urls)),
Some(VideoListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "reference_videos",
value: "GmiCloud only supports video URLs, not media file tokens".to_string(),
}))
}
}
}
#[cfg(test)]
mod tests {
use gmicloud_client::requests::api::video::seedance_2_0_260128::api::{
Seedance20Ratio, Seedance20Resolution,
};
use tokens::tokens::media_files::MediaFileToken;
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
use crate::api::router_provider::RouterProvider;
use crate::api::video_list_ref::VideoListRef;
use crate::api::audio_list_ref::AudioListRef;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
use super::*;
mod field_conversions {
use super::*;
#[test]
fn prompt_passed_through() {
let req = unwrap_request(make_builder(|b| { b.prompt = Some("test prompt".to_string()); }));
assert_eq!(req.request.prompt, "test prompt");
}
#[test]
fn duration_passed_through() {
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); }));
assert_eq!(req.request.duration, Some(10));
}
#[test]
fn duration_clamped_to_min() {
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(1); }));
assert_eq!(req.request.duration, Some(4));
}
#[test]
fn duration_clamped_to_max() {
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); }));
assert_eq!(req.request.duration, Some(15));
}
#[test]
fn duration_none_stays_none() {
let req = unwrap_request(make_builder(|b| { b.duration_seconds = None; }));
assert_eq!(req.request.duration, None);
}
}
mod aspect_ratio_tests {
use super::*;
#[test]
fn landscape_16x9() {
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::WideSixteenByNine); }));
assert_eq!(req.request.ratio, Some(Seedance20Ratio::Landscape16x9));
}
#[test]
fn portrait_9x16() {
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::TallNineBySixteen); }));
assert_eq!(req.request.ratio, Some(Seedance20Ratio::Portrait9x16));
}
#[test]
fn square() {
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::Square); }));
assert_eq!(req.request.ratio, Some(Seedance20Ratio::Square));
}
#[test]
fn four_by_three() {
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::WideFourByThree); }));
assert_eq!(req.request.ratio, Some(Seedance20Ratio::Standard4x3));
}
#[test]
fn unsupported_maps_to_adaptive() {
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::WideThreeByTwo); }));
assert_eq!(req.request.ratio, Some(Seedance20Ratio::Adaptive));
}
#[test]
fn auto_maps_to_none() {
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::Auto); }));
assert_eq!(req.request.ratio, None);
}
}
mod resolution_tests {
use super::*;
#[test]
fn res_480p() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourEightyP); }));
assert_eq!(req.request.resolution, Some(Seedance20Resolution::FourEightyP));
}
#[test]
fn res_720p() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::SevenTwentyP); }));
assert_eq!(req.request.resolution, Some(Seedance20Resolution::SevenTwentyP));
}
#[test]
fn res_1080p() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::TenEightyP); }));
assert_eq!(req.request.resolution, Some(Seedance20Resolution::TenEightyP));
}
#[test]
fn none_stays_none() {
let req = unwrap_request(make_builder(|_| {}));
assert!(req.request.resolution.is_none());
}
}
mod media_url_tests {
use super::*;
#[test]
fn start_frame_url_passed_through() {
let req = unwrap_request(make_builder(|b| {
b.start_frame = Some(ImageRef::Url("https://example.com/start.png".to_string()));
}));
assert_eq!(req.request.first_frame, Some("https://example.com/start.png".to_string()));
}
#[test]
fn media_file_token_rejected_for_start_frame() {
let result = build_gmicloud_seedance_2p0_u(GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_test".to_string()))),
..base_builder()
});
assert!(result.is_err());
}
#[test]
fn reference_image_urls_passed_through() {
let urls = vec!["https://example.com/a.png".to_string(), "https://example.com/b.png".to_string()];
let req = unwrap_request(make_builder(|b| {
b.reference_images = Some(ImageListRef::Urls(urls.clone()));
}));
assert_eq!(req.request.reference_images, Some(urls));
}
#[test]
fn reference_image_tokens_rejected() {
let result = build_gmicloud_seedance_2p0_u(GenerateVideoRequestBuilder {
reference_images: Some(ImageListRef::MediaFileTokens(vec![MediaFileToken::new("mf_a".to_string())])),
..base_builder()
});
assert!(result.is_err());
}
#[test]
fn reference_video_urls_passed_through() {
let urls = vec!["https://example.com/v.mp4".to_string()];
let req = unwrap_request(make_builder(|b| {
b.reference_videos = Some(VideoListRef::Urls(urls.clone()));
}));
assert_eq!(req.request.reference_videos, Some(urls));
}
#[test]
fn reference_audio_urls_passed_through() {
let urls = vec!["https://example.com/a.wav".to_string()];
let req = unwrap_request(make_builder(|b| {
b.reference_audio = Some(AudioListRef::Urls(urls.clone()));
}));
assert_eq!(req.request.reference_audios, Some(urls));
}
}
fn base_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Ultra,
provider: RouterProvider::GmiCloud,
duration_seconds: Some(5),
video_batch_count: Some(1),
..Default::default()
}
}
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
let mut builder = base_builder();
f(&mut builder);
builder
}
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> GmiCloudSeedance2p0UltraRequestState {
let result = build_gmicloud_seedance_2p0_u(builder).expect("build should succeed");
match result {
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0Ultra(state)) => state,
_ => panic!("expected GmiCloudSeedance2p0G request"),
}
}
}
fn resolve_url_list_from_audios(
list_ref: Option<AudioListRef>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
match list_ref {
None => Ok(None),
Some(AudioListRef::Urls(urls)) => Ok(Some(urls)),
Some(AudioListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "reference_audios",
value: "GmiCloud only supports audio URLs, not media file tokens".to_string(),
}))
}
}
}
@@ -1,133 +0,0 @@
use gmicloud_client::traits::gmicloud_request_cost_calculator_trait::GmiCloudRequestCostCalculator;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0UltraRequestState;
pub struct GmiCloudSeedance2p0UltraCostState {
request: GmiCloudSeedance2p0UltraRequestState,
}
impl GmiCloudSeedance2p0UltraCostState {
pub fn from_request(request: &GmiCloudSeedance2p0UltraRequestState) -> Self {
Self { request: request.clone() }
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
let cost_in_usd_cents = self.request.request.calculate_cost_in_cents();
VideoGenerationCostEstimate {
cost_in_credits: None,
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
#[cfg(test)]
mod tests {
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::router_provider::RouterProvider;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
mod pricing_720p {
use super::*;
#[test]
fn default_5s() {
// 720p: 5.2 ¢/s * 5 = 26¢
assert_eq!(cost_cents(None, 5), 26);
}
#[test]
fn default_10s() {
assert_eq!(cost_cents(None, 10), 52);
}
#[test]
fn explicit_720p_5s() {
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5), 26);
}
}
mod pricing_480p {
use super::*;
#[test]
fn p480_5s() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5), 12);
}
#[test]
fn p480_10s() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 10), 24);
}
#[test]
fn p480_15s() {
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 15), 36);
}
}
mod pricing_1080p {
use super::*;
#[test]
fn p1080_5s() {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 5), 58);
}
#[test]
fn p1080_10s() {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 10), 116);
}
#[test]
fn p1080_15s() {
assert_eq!(cost_cents(Some(RouterResolution::TenEightyP), 15), 174);
}
}
mod relative_pricing {
use super::*;
#[test]
fn higher_resolution_costs_more() {
let c480 = cost_cents(Some(RouterResolution::FourEightyP), 10);
let c720 = cost_cents(Some(RouterResolution::SevenTwentyP), 10);
let c1080 = cost_cents(Some(RouterResolution::TenEightyP), 10);
assert!(c480 < c720, "480p ({c480}) should be < 720p ({c720})");
assert!(c720 < c1080, "720p ({c720}) should be < 1080p ({c1080})");
}
#[test]
fn longer_duration_costs_more() {
let c5 = cost_cents(None, 5);
let c10 = cost_cents(None, 10);
let c15 = cost_cents(None, 15);
assert!(c5 < c10);
assert!(c10 < c15);
}
}
fn cost_cents(resolution: Option<RouterResolution>, duration_seconds: u16) -> u64 {
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Ultra,
provider: RouterProvider::GmiCloud,
resolution,
duration_seconds: Some(duration_seconds),
video_batch_count: Some(1),
..Default::default()
};
builder.build2()
.expect("build2 should succeed")
.estimate_cost()
.expect("estimate_cost should succeed")
.cost_in_usd_cents
.unwrap()
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,98 +0,0 @@
use gmicloud_client::requests::api::video::seedance_2_0_260128::api::Seedance20Request;
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_video::generate_video_response::{
GenerateVideoResponse, GmiCloudVideoResponsePayload,
};
#[derive(Clone, Debug)]
pub struct GmiCloudSeedance2p0UltraRequestState {
pub request: Seedance20Request,
}
impl GmiCloudSeedance2p0UltraRequestState {
pub async fn send(&self, client: &RouterGmiCloudClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let response = self.request.send_request(&client.api_key)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::GmiCloud(err)))?;
Ok(GenerateVideoResponse::GmiCloud(GmiCloudVideoResponsePayload {
request_id: response.request_id,
}))
}
}
#[cfg(test)]
mod tests {
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::image_ref::ImageRef;
use crate::api::router_provider::RouterProvider;
use crate::client::router_client::RouterClient;
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use gmicloud_client::creds::gmicloud_api_key::GmiCloudApiKey;
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
#[tokio::test]
#[ignore] // requires real API key, incurs costs
async fn test_text_to_video_720p() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A corgi running through a field of wildflowers at sunset.".to_string()),
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
resolution: Some(RouterResolution::SevenTwentyP),
duration_seconds: Some(5),
..gmicloud_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::GmiCloud(_)));
}
#[tokio::test]
#[ignore] // requires real API key, incurs costs
async fn test_image_to_video_480p() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("The dog starts running and splashing in the lake.".to_string()),
start_frame: Some(ImageRef::Url(JUNO_AT_LAKE_IMAGE_URL.to_string())),
resolution: Some(RouterResolution::FourEightyP),
duration_seconds: Some(5),
..gmicloud_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::GmiCloud(_)));
}
fn gmicloud_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Ultra,
provider: RouterProvider::GmiCloud,
video_batch_count: Some(1),
..Default::default()
}
}
fn get_gmicloud_client() -> RouterClient {
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/gmicloud_api_key.txt")
.expect("Failed to read GmiCloud API key");
let api_key = GmiCloudApiKey::from_str(&secret);
RouterClient::GmiCloud(RouterGmiCloudClient::new(api_key))
}
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
let client = get_gmicloud_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let request = match draft_or_request {
VideoGenerationDraftOrRequest::Request(r) => r,
_ => panic!("expected Request variant (GmiCloud skips draft)"),
};
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateVideoResponse::GmiCloud(p) => println!("request_id={}", p.request_id),
other => println!("response: {:?}", other),
}
response
}
}
@@ -149,7 +149,7 @@ fn plan_output_resolution(
}
}
// Seedance2p0 supports batch counts of 1, 2, and 4 only.
// Seedance2p0 supports batch counts of 1 through 4.
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
@@ -159,7 +159,9 @@ fn plan_batch_count(
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(KinoviBatchCount::One),
2 => Ok(KinoviBatchCount::Two),
3 => Ok(KinoviBatchCount::Three),
4 => Ok(KinoviBatchCount::Four),
// 5 and above:
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
@@ -167,12 +169,8 @@ fn plan_batch_count(
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
Ok(if count < 4 { KinoviBatchCount::Four } else { KinoviBatchCount::Four })
}
RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(if count < 4 { KinoviBatchCount::Two } else { KinoviBatchCount::Four })
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(KinoviBatchCount::Four),
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(KinoviBatchCount::Four),
},
}
}
@@ -291,6 +289,13 @@ mod tests {
assert!(matches!(draft.batch_count, KinoviBatchCount::Two));
}
#[test]
fn batch_count_three() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(3), ..kinovi_web_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Three));
}
#[test]
fn batch_count_four() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..kinovi_web_builder() };
@@ -119,6 +119,13 @@ mod tests {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Two), 494);
}
#[test]
fn cost_720p_batch_3() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Three), 198);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Three), 247);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Three), 741);
}
#[test]
fn cost_720p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Four), 264);
@@ -145,6 +152,11 @@ mod tests {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 62);
}
#[test]
fn cost_480p_batch_3() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Three), 93);
}
#[test]
fn cost_480p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 124);
@@ -169,6 +181,11 @@ mod tests {
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Two), 371);
}
#[test]
fn cost_1080p_batch_3() {
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Three), 556);
}
#[test]
fn cost_1080p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Four), 741);
@@ -196,6 +213,11 @@ mod tests {
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::Two), 824);
}
#[test]
fn cost_4k_batch_3() {
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::Three), 1235);
}
#[test]
fn cost_4k_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::Four), 1647);
@@ -163,7 +163,7 @@ fn plan_output_resolution(
}
}
// Seedance2p0Fast supports batch counts of 1, 2, and 4 only.
// Seedance2p0Fast supports batch counts of 1 through 4.
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
@@ -173,7 +173,9 @@ fn plan_batch_count(
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(KinoviBatchCount::One),
2 => Ok(KinoviBatchCount::Two),
3 => Ok(KinoviBatchCount::Three),
4 => Ok(KinoviBatchCount::Four),
// 5 and above:
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
@@ -181,12 +183,8 @@ fn plan_batch_count(
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
Ok(if count < 4 { KinoviBatchCount::Four } else { KinoviBatchCount::Four })
}
RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(if count < 4 { KinoviBatchCount::Two } else { KinoviBatchCount::Four })
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(KinoviBatchCount::Four),
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(KinoviBatchCount::Four),
},
}
}
@@ -307,6 +305,13 @@ mod tests {
assert!(matches!(draft.batch_count, KinoviBatchCount::Two));
}
#[test]
fn batch_count_three() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(3), ..kinovi_web_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Three));
}
#[test]
fn batch_count_four() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..kinovi_web_fast_builder() };
@@ -119,6 +119,11 @@ mod tests {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 116);
}
#[test]
fn cost_720p_batch_3() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Three), 173);
}
#[test]
fn cost_720p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 231);
@@ -143,6 +148,11 @@ mod tests {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 58);
}
#[test]
fn cost_480p_batch_3() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Three), 87);
}
#[test]
fn cost_480p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 116);
@@ -73,10 +73,15 @@ impl KinoviSeedance2p5CostState {
}
};
// NB: The kinovi calculator clamps too; clamping here as well keeps this
// state's math correct even if it's read directly.
// NB: The kinovi calculator clamps measured values and bills the
// worst-case maximum for unknown (None) or zero inputs. Zero passes
// through unclamped so that worst-case fallback applies — clamping it up
// to the minimum here would understate the cost.
let total_input_seconds = self.total_input_seconds
.map(|seconds| seconds.clamp(MIN_BILLED_INPUT_SECONDS, MAX_BILLED_INPUT_SECONDS));
.map(|seconds| match seconds {
0 => 0,
measured => measured.clamp(MIN_BILLED_INPUT_SECONDS, MAX_BILLED_INPUT_SECONDS),
});
let pricing_request = GenerateSeedance2p5Request {
prompt: String::new(),
@@ -1,5 +1,4 @@
pub mod artcraft;
pub mod fal;
pub mod gmicloud;
pub mod grok_api;
pub mod kinovi;
@@ -41,10 +41,6 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p0::cost::Ar
use crate::generate::generate_video::providers::artcraft::seedance_2p0::request::ArtcraftSeedance2p0RequestState;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_fast::cost::ArtcraftSeedance2p0FastCostState;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_fast::request::ArtcraftSeedance2p0FastRequestState;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_u::cost::ArtcraftSeedance2p0UltraCostState;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_u::request::ArtcraftSeedance2p0UltraRequestState;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_u_fast::cost::ArtcraftSeedance2p0UltraFastCostState;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_u_fast::request::ArtcraftSeedance2p0UltraFastRequestState;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp::cost::ArtcraftSeedance2p0BytePlusCostState;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp::request::ArtcraftSeedance2p0BytePlusRequestState;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_fast::cost::ArtcraftSeedance2p0BytePlusFastCostState;
@@ -129,10 +125,6 @@ use crate::generate::generate_video::providers::fal::vidu_q3::cost::FalViduQ3Cos
use crate::generate::generate_video::providers::fal::vidu_q3::request::FalViduQ3RequestState;
use crate::generate::generate_video::providers::fal::vidu_q3_turbo::cost::FalViduQ3TurboCostState;
use crate::generate::generate_video::providers::fal::vidu_q3_turbo::request::FalViduQ3TurboRequestState;
use crate::generate::generate_video::providers::gmicloud::seedance_2p0_g::cost::GmiCloudSeedance2p0UltraCostState;
use crate::generate::generate_video::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0UltraRequestState;
use crate::generate::generate_video::providers::gmicloud::seedance_2p0_fast_g::cost::GmiCloudSeedance2p0UltraFastCostState;
use crate::generate::generate_video::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0UltraFastRequestState;
use crate::generate::generate_video::providers::grok_api::grok_imagine_video::cost::GrokApiGrokImagineVideoCostState;
use crate::generate::generate_video::providers::grok_api::grok_imagine_video::request::GrokApiGrokImagineVideoRequestState;
use crate::generate::generate_video::providers::grok_api::grok_imagine_video_1p5::cost::GrokApiGrokImagineVideo1p5CostState;
@@ -171,8 +163,6 @@ pub enum VideoGenerationRequest {
ArtcraftSeedance1p5Pro(ArtcraftSeedance1p5ProRequestState),
ArtcraftSeedance2p0(ArtcraftSeedance2p0RequestState),
ArtcraftSeedance2p0Fast(ArtcraftSeedance2p0FastRequestState),
ArtcraftSeedance2p0Ultra(ArtcraftSeedance2p0UltraRequestState),
ArtcraftSeedance2p0UltraFast(ArtcraftSeedance2p0UltraFastRequestState),
ArtcraftSeedance2p0BytePlus(ArtcraftSeedance2p0BytePlusRequestState),
ArtcraftSeedance2p0BytePlusFast(ArtcraftSeedance2p0BytePlusFastRequestState),
ArtcraftSeedance2p0BytePlusUltra(ArtcraftSeedance2p0BytePlusUltraRequestState),
@@ -215,8 +205,6 @@ pub enum VideoGenerationRequest {
FalVeo3p1Lite(FalVeo3p1LiteRequestState),
FalViduQ3(FalViduQ3RequestState),
FalViduQ3Turbo(FalViduQ3TurboRequestState),
GmiCloudSeedance2p0Ultra(GmiCloudSeedance2p0UltraRequestState),
GmiCloudSeedance2p0UltraFast(GmiCloudSeedance2p0UltraFastRequestState),
GrokApiGrokImagineVideo(GrokApiGrokImagineVideoRequestState),
GrokApiGrokImagineVideo1p5(GrokApiGrokImagineVideo1p5RequestState),
KinoviHappyHorse1p0(KinoviHappyHorse1p0RequestState),
@@ -250,8 +238,6 @@ impl VideoGenerationRequest {
Self::ArtcraftSeedance1p5Pro(_) => RouterProvider::Artcraft,
Self::ArtcraftSeedance2p0(_) => RouterProvider::Artcraft,
Self::ArtcraftSeedance2p0Fast(_) => RouterProvider::Artcraft,
Self::ArtcraftSeedance2p0Ultra(_) => RouterProvider::Artcraft,
Self::ArtcraftSeedance2p0UltraFast(_) => RouterProvider::Artcraft,
Self::ArtcraftSeedance2p0BytePlus(_) => RouterProvider::Artcraft,
Self::ArtcraftSeedance2p0BytePlusFast(_) => RouterProvider::Artcraft,
Self::ArtcraftSeedance2p0BytePlusUltra(_) => RouterProvider::Artcraft,
@@ -294,8 +280,6 @@ impl VideoGenerationRequest {
Self::FalVeo3p1Lite(_) => RouterProvider::Fal,
Self::FalViduQ3(_) => RouterProvider::Fal,
Self::FalViduQ3Turbo(_) => RouterProvider::Fal,
Self::GmiCloudSeedance2p0Ultra(_) => RouterProvider::GmiCloud,
Self::GmiCloudSeedance2p0UltraFast(_) => RouterProvider::GmiCloud,
Self::GrokApiGrokImagineVideo(_) => RouterProvider::GrokApi,
Self::GrokApiGrokImagineVideo1p5(_) => RouterProvider::GrokApi,
Self::KinoviHappyHorse1p0(_) => RouterProvider::KinoviWeb,
@@ -329,8 +313,6 @@ impl VideoGenerationRequest {
VideoGenerationRequest::ArtcraftSeedance1p5Pro(request) => Ok(ArtcraftSeedance1p5ProCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftSeedance2p0(request) => Ok(ArtcraftSeedance2p0CostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftSeedance2p0Fast(request) => Ok(ArtcraftSeedance2p0FastCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftSeedance2p0Ultra(request) => Ok(ArtcraftSeedance2p0UltraCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftSeedance2p0UltraFast(request) => Ok(ArtcraftSeedance2p0UltraFastCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftSeedance2p0BytePlus(request) => Ok(ArtcraftSeedance2p0BytePlusCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusFast(request) => Ok(ArtcraftSeedance2p0BytePlusFastCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltra(request) => Ok(ArtcraftSeedance2p0BytePlusUltraCostState::from_request(request).estimate_cost()),
@@ -373,8 +355,6 @@ impl VideoGenerationRequest {
VideoGenerationRequest::FalVeo3p1Lite(request) => Ok(FalVeo3p1LiteCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::FalViduQ3(request) => Ok(FalViduQ3CostState::from_request(request).estimate_cost()),
VideoGenerationRequest::FalViduQ3Turbo(request) => Ok(FalViduQ3TurboCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::GmiCloudSeedance2p0Ultra(request) => Ok(GmiCloudSeedance2p0UltraCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::GmiCloudSeedance2p0UltraFast(request) => Ok(GmiCloudSeedance2p0UltraFastCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::GrokApiGrokImagineVideo(request) => Ok(GrokApiGrokImagineVideoCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::GrokApiGrokImagineVideo1p5(request) => GrokApiGrokImagineVideo1p5CostState::from_request(request).estimate_cost(),
VideoGenerationRequest::KinoviHappyHorse1p0(request) => Ok(KinoviHappyHorse1p0CostState::from_request(request).estimate_cost()),
@@ -466,14 +446,6 @@ impl VideoGenerationRequest {
let client_ref = client.get_artcraft_client_ref()?;
request.send(client_ref).await
},
VideoGenerationRequest::ArtcraftSeedance2p0Ultra(request) => {
let client_ref = client.get_artcraft_client_ref()?;
request.send(client_ref).await
},
VideoGenerationRequest::ArtcraftSeedance2p0UltraFast(request) => {
let client_ref = client.get_artcraft_client_ref()?;
request.send(client_ref).await
},
VideoGenerationRequest::ArtcraftSeedance2p0BytePlus(request) => {
let client_ref = client.get_artcraft_client_ref()?;
request.send(client_ref).await
@@ -642,14 +614,6 @@ impl VideoGenerationRequest {
let client_ref = client.get_fal_client_ref()?;
request.send(client_ref).await
},
VideoGenerationRequest::GmiCloudSeedance2p0Ultra(request) => {
let client_ref = client.get_gmicloud_client_ref()?;
request.send(client_ref).await
},
VideoGenerationRequest::GmiCloudSeedance2p0UltraFast(request) => {
let client_ref = client.get_gmicloud_client_ref()?;
request.send(client_ref).await
},
VideoGenerationRequest::GrokApiGrokImagineVideo(request) => {
let client_ref = client.get_grok_api_client_ref()?;
request.send(client_ref).await
@@ -61,6 +61,7 @@ pub enum KinoviSeedance2p0OutputResolution {
pub enum KinoviSeedance2p0BatchCount {
One,
Two,
Three,
Four,
}
@@ -124,6 +125,7 @@ impl GenerateSeedance2p0Request {
let batch_multiplier: u64 = match self.batch_count {
None | Some(KinoviSeedance2p0BatchCount::One) => 1,
Some(KinoviSeedance2p0BatchCount::Two) => 2,
Some(KinoviSeedance2p0BatchCount::Three) => 3,
Some(KinoviSeedance2p0BatchCount::Four) => 4,
};
@@ -235,6 +237,7 @@ fn map_batch_count(bc: Option<KinoviSeedance2p0BatchCount>) -> KinoviBatchCountR
match bc {
Some(KinoviSeedance2p0BatchCount::One) | None => KinoviBatchCountRaw::One,
Some(KinoviSeedance2p0BatchCount::Two) => KinoviBatchCountRaw::Two,
Some(KinoviSeedance2p0BatchCount::Three) => KinoviBatchCountRaw::Three,
Some(KinoviSeedance2p0BatchCount::Four) => KinoviBatchCountRaw::Four,
}
}
@@ -689,6 +692,13 @@ mod tests {
assert_eq!(batch2, base * 2);
}
#[test]
fn batch_3_triples() {
let base = r720(5).calculate_costs().total_cost.kinovi_credits;
let batch3 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Three)).calculate_costs().total_cost.kinovi_credits;
assert_eq!(batch3, base * 3);
}
#[test]
fn batch_4_quadruples() {
let base = r720(5).calculate_costs().total_cost.kinovi_credits;
@@ -59,6 +59,7 @@ pub enum KinoviSeedance2p0FastOutputResolution {
pub enum KinoviSeedance2p0FastBatchCount {
One,
Two,
Three,
Four,
}
@@ -114,6 +115,7 @@ impl GenerateSeedance2p0FastRequest {
let batch_multiplier: u64 = match self.batch_count {
None | Some(KinoviSeedance2p0FastBatchCount::One) => 1,
Some(KinoviSeedance2p0FastBatchCount::Two) => 2,
Some(KinoviSeedance2p0FastBatchCount::Three) => 3,
Some(KinoviSeedance2p0FastBatchCount::Four) => 4,
};
@@ -223,6 +225,7 @@ fn map_batch_count(bc: Option<KinoviSeedance2p0FastBatchCount>) -> KinoviBatchCo
match bc {
Some(KinoviSeedance2p0FastBatchCount::One) | None => KinoviBatchCountRaw::One,
Some(KinoviSeedance2p0FastBatchCount::Two) => KinoviBatchCountRaw::Two,
Some(KinoviSeedance2p0FastBatchCount::Three) => KinoviBatchCountRaw::Three,
Some(KinoviSeedance2p0FastBatchCount::Four) => KinoviBatchCountRaw::Four,
}
}
@@ -465,6 +468,13 @@ mod tests {
assert_eq!(batch2, base * 2);
}
#[test]
fn batch_3_triples() {
let base = r720(5).calculate_costs().total_cost.kinovi_credits;
let batch3 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Three)).calculate_costs().total_cost.kinovi_credits;
assert_eq!(batch3, base * 3);
}
#[test]
fn batch_4_quadruples() {
let base = r720(5).calculate_costs().total_cost.kinovi_credits;
@@ -145,9 +145,12 @@ impl GenerateSeedance2p5Request {
Some(KinoviSeedance2p5OutputResolution::FourEightyP) => 16u64,
Some(KinoviSeedance2p5OutputResolution::SevenTwentyP) | None => 35u64,
};
let input_seconds = self.total_input_seconds
.unwrap_or(MAX_BILLED_INPUT_SECONDS)
.clamp(MIN_BILLED_INPUT_SECONDS, MAX_BILLED_INPUT_SECONDS);
// An unknown (None) or zero input duration bills the worst-case
// maximum — never default toward the minimum.
let input_seconds = match self.total_input_seconds {
None | Some(0) => MAX_BILLED_INPUT_SECONDS,
Some(seconds) => seconds.clamp(MIN_BILLED_INPUT_SECONDS, MAX_BILLED_INPUT_SECONDS),
};
let seconds = u64::from(self.duration_seconds) + u64::from(input_seconds);
(rate, seconds)
} else {
@@ -483,11 +486,12 @@ mod tests {
fn video_reference_rate_is_cheaper_per_second() {
// 16 < 26 and 35 < 59: the with-references rate is lower per billed
// second (the input seconds are where the money goes). Compare at
// zero input seconds — the unknown-duration default assumes the
// 30-second maximum, which would swamp the rate comparison.
assert!(video_ref_480(10, Some(0)).calculate_costs().kinovi_credits
// the minimum billed input (1s clamps to 4) — unknown or zero input
// assumes the 30-second maximum, which would swamp the rate
// comparison.
assert!(video_ref_480(10, Some(1)).calculate_costs().kinovi_credits
< t2v_480(10).calculate_costs().kinovi_credits);
assert!(video_ref_720(10, Some(0)).calculate_costs().kinovi_credits
assert!(video_ref_720(10, Some(1)).calculate_costs().kinovi_credits
< t2v_720(10).calculate_costs().kinovi_credits);
}
@@ -78,8 +78,6 @@ fn video_model_to_common_model_type(model: RouterVideoModel) -> Option<CommonMod
RouterVideoModel::ViduQ3Turbo => Some(CommonModelType::ViduQ3Turbo),
RouterVideoModel::Seedance2p0BytePlus => Some(CommonModelType::Seedance2p0BytePlus),
RouterVideoModel::Seedance2p0BytePlusFast => Some(CommonModelType::Seedance2p0BytePlusFast),
RouterVideoModel::Seedance2p0Ultra => Some(CommonModelType::Seedance2p0Ultra),
RouterVideoModel::Seedance2p0UltraFast => Some(CommonModelType::Seedance2p0UltraFast),
RouterVideoModel::Seedance2p0BytePlusUltra => Some(CommonModelType::Seedance2p0BytePlusUltra),
RouterVideoModel::Seedance2p0BytePlusUltraFast => Some(CommonModelType::Seedance2p0BytePlusUltraFast),
RouterVideoModel::Seedance2p0Mini => Some(CommonModelType::Seedance2p0Mini),
@@ -91,6 +89,7 @@ fn video_model_to_common_model_type(model: RouterVideoModel) -> Option<CommonMod
RouterVideoModel::GrokImagineVideo1p5 => Some(CommonModelType::GrokImagineVideo1p5),
RouterVideoModel::Seedance2p5Preview => Some(CommonModelType::Seedance2p5Preview),
RouterVideoModel::Seedance2p5 => Some(CommonModelType::Seedance2p5),
RouterVideoModel::Seedance2p5Ultra => Some(CommonModelType::Seedance2p5Ultra),
}
}
@@ -164,6 +163,7 @@ mod tests {
duration_seconds: None,
video_batch_count: None,
generate_audio: None,
total_reference_video_input_seconds: None,
request_mismatch_mitigation_strategy:
artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
@@ -177,8 +177,6 @@ pub enum ListVideoModelsVideoModel {
Seedance2p0Fast,
Seedance2p0BytePlus,
Seedance2p0BytePlusFast,
Seedance2p0Ultra,
Seedance2p0UltraFast,
Seedance2p0BytePlusUltra,
Seedance2p0BytePlusUltraFast,
Seedance2p0Mini,
@@ -216,8 +214,6 @@ impl From<ApiClientVideoModel> for ListVideoModelsVideoModel {
ApiClientVideoModel::Seedance2p0Fast => Self::Seedance2p0Fast,
ApiClientVideoModel::Seedance2p0BytePlus => Self::Seedance2p0BytePlus,
ApiClientVideoModel::Seedance2p0BytePlusFast => Self::Seedance2p0BytePlusFast,
ApiClientVideoModel::Seedance2p0Ultra => Self::Seedance2p0Ultra,
ApiClientVideoModel::Seedance2p0UltraFast => Self::Seedance2p0UltraFast,
ApiClientVideoModel::Seedance2p0BytePlusUltra => Self::Seedance2p0BytePlusUltra,
ApiClientVideoModel::Seedance2p0BytePlusUltraFast => Self::Seedance2p0BytePlusUltraFast,
ApiClientVideoModel::Seedance2p0Mini => Self::Seedance2p0Mini,
@@ -257,8 +253,6 @@ impl From<ListVideoModelsVideoModel> for String {
ListVideoModelsVideoModel::Seedance2p0Fast => "seedance_2p0_fast".to_string(),
ListVideoModelsVideoModel::Seedance2p0BytePlus => "seedance_2p0_bp".to_string(),
ListVideoModelsVideoModel::Seedance2p0BytePlusFast => "seedance_2p0_bp_fast".to_string(),
ListVideoModelsVideoModel::Seedance2p0Ultra => "seedance_2p0_u".to_string(),
ListVideoModelsVideoModel::Seedance2p0UltraFast => "seedance_2p0_u_fast".to_string(),
ListVideoModelsVideoModel::Seedance2p0BytePlusUltra => "seedance_2p0_bpu".to_string(),
ListVideoModelsVideoModel::Seedance2p0BytePlusUltraFast => "seedance_2p0_bpu_fast".to_string(),
ListVideoModelsVideoModel::Seedance2p0Mini => "seedance_2p0_mini".to_string(),
@@ -1,3 +1,8 @@
// Deprecated model variants (e.g. the Seedance 2.0 Ultra tier) must keep
// their classification arms here so historical rows still map; suppress the
// deprecation lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
#[cfg(test)]
@@ -1,3 +1,8 @@
// Deprecated model variants (e.g. the Seedance 2.0 Ultra tier) must keep
// their classification arms here so historical rows still map; suppress the
// deprecation lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
#[cfg(test)]
@@ -1,3 +1,8 @@
// Deprecated model variants (e.g. the Seedance 2.0 Ultra tier) must keep
// their classification arms here so historical rows still map; suppress the
// deprecation lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
#[cfg(test)]
@@ -1,3 +1,7 @@
// The deprecated Seedance 2.0 Ultra variants must stay referenced by this
// enum's own coders, variant lists, and tests; suppress the lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
#[cfg(test)]
@@ -204,8 +208,10 @@ pub enum InferenceModelType {
Seedance2p0BytePlus,
#[serde(rename = "seedance_2p0_bp_fast")]
Seedance2p0BytePlusFast,
#[deprecated(note = "Never launched and unroutable; kept only so historical database rows keep decoding")]
#[serde(rename = "seedance_2p0_u")]
Seedance2p0Ultra,
#[deprecated(note = "Never launched and unroutable; kept only so historical database rows keep decoding")]
#[serde(rename = "seedance_2p0_u_fast")]
Seedance2p0UltraFast,
#[serde(rename = "seedance_2p0_bpu")]
@@ -1,3 +1,8 @@
// Deprecated model variants (e.g. the Seedance 2.0 Ultra tier) must keep
// their classification arms here so historical rows still map; suppress the
// deprecation lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
#[cfg(test)]
@@ -1,3 +1,8 @@
// Deprecated model variants (e.g. the Seedance 2.0 Ultra tier) must keep
// their classification arms here so historical rows still map; suppress the
// deprecation lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
use serde::Deserialize;
@@ -1,3 +1,8 @@
// Deprecated model variants (e.g. the Seedance 2.0 Ultra tier) must keep
// their classification arms here so historical rows still map; suppress the
// deprecation lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
use serde::Deserialize;
@@ -1,3 +1,8 @@
// Deprecated model variants (e.g. the Seedance 2.0 Ultra tier) must keep
// their classification arms here so historical rows still map; suppress the
// deprecation lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
#[cfg(test)]
@@ -1,3 +1,8 @@
// Deprecated model variants (e.g. the Seedance 2.0 Ultra tier) must keep
// their classification arms here so historical rows still map; suppress the
// deprecation lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
use serde::Deserialize;
@@ -1,3 +1,8 @@
// Deprecated model variants (e.g. the Seedance 2.0 Ultra tier) must keep
// their classification arms here so historical rows still map; suppress the
// deprecation lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
#[cfg(test)]
@@ -1,3 +1,8 @@
// Deprecated model variants (e.g. the Seedance 2.0 Ultra tier) must keep
// their classification arms here so historical rows still map; suppress the
// deprecation lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
#[cfg(test)]
@@ -1,3 +1,7 @@
// The deprecated Seedance 2.0 Ultra variants must stay referenced by this
// enum's own coders, variant lists, and tests; suppress the lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
use serde::Deserialize;
@@ -53,8 +57,10 @@ pub enum UploadedVideoNoteReportedModelType {
Seedance2p0BytePlus,
#[serde(rename = "seedance_2p0_bp_fast")]
Seedance2p0BytePlusFast,
#[deprecated(note = "Never launched and unroutable; kept only so historical database rows keep decoding")]
#[serde(rename = "seedance_2p0_u")]
Seedance2p0Ultra,
#[deprecated(note = "Never launched and unroutable; kept only so historical database rows keep decoding")]
#[serde(rename = "seedance_2p0_u_fast")]
Seedance2p0UltraFast,
#[serde(rename = "seedance_2p0_bpu")]
@@ -1,3 +1,7 @@
// The deprecated Seedance 2.0 Ultra variants must stay referenced by this
// enum's own coders, variant lists, and tests; suppress the lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
use serde::Deserialize;
@@ -53,8 +57,10 @@ pub enum UploadedVideoDetectedModelType {
Seedance2p0BytePlus,
#[serde(rename = "seedance_2p0_bp_fast")]
Seedance2p0BytePlusFast,
#[deprecated(note = "Never launched and unroutable; kept only so historical database rows keep decoding")]
#[serde(rename = "seedance_2p0_u")]
Seedance2p0Ultra,
#[deprecated(note = "Never launched and unroutable; kept only so historical database rows keep decoding")]
#[serde(rename = "seedance_2p0_u_fast")]
Seedance2p0UltraFast,
#[serde(rename = "seedance_2p0_bpu")]
@@ -1,3 +1,8 @@
// Deprecated model variants (e.g. the Seedance 2.0 Ultra tier) must keep
// their classification arms here so historical rows still map; suppress the
// deprecation lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
#[cfg(test)]
@@ -1,3 +1,7 @@
// The deprecated Seedance 2.0 Ultra variants must stay referenced by this
// enum's own coders, variant lists, and tests; suppress the lint file-wide.
#![allow(deprecated)]
use std::collections::BTreeSet;
#[cfg(test)]
@@ -175,8 +179,10 @@ pub enum CommonModelType {
Seedance2p0BytePlus,
#[serde(rename = "seedance_2p0_bp_fast")]
Seedance2p0BytePlusFast,
#[deprecated(note = "Never launched and unroutable; kept only so historical database rows keep decoding")]
#[serde(rename = "seedance_2p0_u")]
Seedance2p0Ultra,
#[deprecated(note = "Never launched and unroutable; kept only so historical database rows keep decoding")]
#[serde(rename = "seedance_2p0_u_fast")]
Seedance2p0UltraFast,
#[serde(rename = "seedance_2p0_bpu")]
@@ -1,3 +1,7 @@
// The deprecated Seedance 2.0 Ultra variants must stay referenced by this
// enum's own coders, variant lists, and tests; suppress the lint file-wide.
#![allow(deprecated)]
use utoipa::ToSchema;
/// Video models available for generation.
@@ -74,9 +78,11 @@ pub enum CommonVideoModel {
#[serde(rename = "seedance_2p0_bp_fast")]
Seedance2p0BytePlusFast,
#[deprecated(note = "Never launched and unroutable; kept only so historical database rows keep decoding")]
#[serde(rename = "seedance_2p0_u")]
Seedance2p0Ultra,
#[deprecated(note = "Never launched and unroutable; kept only so historical database rows keep decoding")]
#[serde(rename = "seedance_2p0_u_fast")]
Seedance2p0UltraFast,
+4 -4
View File
@@ -75,23 +75,23 @@ curl -s -c /tmp/cookies.txt -X POST http://localhost:12345/v1/login \
-H "Content-Type: application/json" \
-d '{"username_or_email":"newuser4","password":"newuser4"}'
# 2. Generate video (GmiCloud Seedance 2.0 Fast Global):
# 2. Generate video (Seedance 2.0 Fast):
# Requires a UUID-format idempotency_token.
curl -s -b /tmp/cookies.txt -X POST http://localhost:12345/v1/omni_gen/generate/video \
-H "Content-Type: application/json" \
-d '{
"model": "seedance_2p0_fast_g",
"model": "seedance_2p0_fast",
"prompt": "a corgi running through a field",
"duration_seconds": 5,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}'
# 3. Generate video (GmiCloud Seedance 2.0 Global, 720p):
# 3. Generate video (Seedance 2.0, 720p):
curl -s -b /tmp/cookies.txt -X POST http://localhost:12345/v1/omni_gen/generate/video \
-H "Content-Type: application/json" \
-d '{
"model": "seedance_2p0_g",
"model": "seedance_2p0",
"prompt": "a corgi running through a field",
"duration_seconds": 5,
"resolution": "seven_twenty_p",
@@ -53,7 +53,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonBitrate::High,
]),
bitrate_default: Some(CommonBitrate::Normal),
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(4),
duration_seconds_max: Some(15),
@@ -100,7 +100,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonBitrate::High,
]),
bitrate_default: Some(CommonBitrate::Normal),
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(4),
duration_seconds_max: Some(15),
@@ -149,7 +149,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonBitrate::High,
]),
bitrate_default: Some(CommonBitrate::Normal),
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(4),
duration_seconds_max: Some(15),
@@ -196,7 +196,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonBitrate::High,
]),
bitrate_default: Some(CommonBitrate::Normal),
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(4),
duration_seconds_max: Some(15),
@@ -245,7 +245,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonBitrate::High,
]),
bitrate_default: Some(CommonBitrate::Normal),
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(4),
duration_seconds_max: Some(15),
@@ -291,7 +291,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonBitrate::Normal,
CommonBitrate::High,
]),
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(4),
duration_seconds_max: Some(15),
@@ -338,7 +338,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonBitrate::High,
]),
bitrate_default: Some(CommonBitrate::Normal),
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(4),
duration_seconds_max: Some(15),
@@ -385,7 +385,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonBitrate::High,
]),
bitrate_default: Some(CommonBitrate::Normal),
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(4),
duration_seconds_max: Some(15),
@@ -432,7 +432,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonBitrate::High,
]),
bitrate_default: Some(CommonBitrate::Normal),
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(4),
duration_seconds_max: Some(15),
@@ -147,8 +147,12 @@ fn map_video_model(model: CommonVideoModel) -> Result<RouterVideoModel, HandlerE
CommonVideoModel::Seedance1p5Pro => RouterVideoModel::Seedance1p5Pro,
CommonVideoModel::Seedance2p0 => RouterVideoModel::Seedance2p0,
CommonVideoModel::Seedance2p0Fast => RouterVideoModel::Seedance2p0Fast,
CommonVideoModel::Seedance2p0Ultra => RouterVideoModel::Seedance2p0Ultra,
CommonVideoModel::Seedance2p0UltraFast => RouterVideoModel::Seedance2p0UltraFast,
// Deprecated: these never launched and have no execution route.
#[allow(deprecated)]
CommonVideoModel::Seedance2p0Ultra
| CommonVideoModel::Seedance2p0UltraFast => {
return Err(HandlerError::InvalidInput(format!("Model is not supported: {:?}", model)));
}
CommonVideoModel::Seedance2p0BytePlus=> RouterVideoModel::Seedance2p0BytePlus,
CommonVideoModel::Seedance2p0BytePlusFast=> RouterVideoModel::Seedance2p0BytePlusFast,
CommonVideoModel::Seedance2p0BytePlusUltra => RouterVideoModel::Seedance2p0BytePlusUltra,
@@ -164,8 +164,6 @@ mod tests {
#[test]
fn ignores_over_limit_prompt_for_excluded_seedance_variants() {
let excluded_models = [
CommonVideoModel::Seedance2p0Ultra,
CommonVideoModel::Seedance2p0UltraFast,
CommonVideoModel::Seedance2p0BytePlusUltra,
CommonVideoModel::Seedance2p0BytePlusUltraFast,
CommonVideoModel::Seedance2p0BytePlusUltraMini,
@@ -83,8 +83,6 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
RouterVideoModel::Seedance2p5Preview => RouterProvider::KinoviWeb,
RouterVideoModel::Seedance2p5 => RouterProvider::KinoviWeb,
RouterVideoModel::Seedance2p5Ultra => RouterProvider::KinoviWeb,
//RouterVideoModel::Seedance2p0Ultra => RouterProvider::GmiCloud,
//RouterVideoModel::Seedance2p0UltraFast => RouterProvider::GmiCloud,
RouterVideoModel::GrokImagineVideo => RouterProvider::GrokApi,
RouterVideoModel::GrokImagineVideo1p5 => RouterProvider::GrokApi,
_ => RouterProvider::Fal,
@@ -12,7 +12,7 @@ use enums::common::generation::common_resolution::CommonResolution;
use enums::common::generation::common_video_model::CommonVideoModel;
use crate::http_server::endpoints::omni_gen::generate::video::tests::support::{
assert_generation_fails_and_charges_nothing, assert_reference_video_charge_then_refund,
assert_generation_fails_and_charges_nothing, assert_reference_video_charge_then_refund, assert_references_charge_more,
assert_successful_generation_charges, assert_variant_charges_premium, Batch, CreditsDelta,
ExpectedCredits, Seconds, TestHarness,
};
@@ -31,18 +31,18 @@ mod seedance_2p0_fast {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(4), Batch(1), ExpectedCredits(21)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(26)),
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(1), ExpectedCredits(31)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(1), ExpectedCredits(36)),
(Some(CommonResolution::FourEightyP), Seconds(8), Batch(1), ExpectedCredits(41)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(1), ExpectedCredits(47)),
(Some(CommonResolution::FourEightyP), Seconds(10), Batch(1), ExpectedCredits(52)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(1), ExpectedCredits(57)),
(Some(CommonResolution::FourEightyP), Seconds(12), Batch(1), ExpectedCredits(62)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(1), ExpectedCredits(67)),
(Some(CommonResolution::FourEightyP), Seconds(14), Batch(1), ExpectedCredits(73)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(1), ExpectedCredits(78)),
(Some(CommonResolution::FourEightyP), Seconds(4), Batch(1), ExpectedCredits(26)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(32)),
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(1), ExpectedCredits(38)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(1), ExpectedCredits(45)),
(Some(CommonResolution::FourEightyP), Seconds(8), Batch(1), ExpectedCredits(51)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(1), ExpectedCredits(58)),
(Some(CommonResolution::FourEightyP), Seconds(10), Batch(1), ExpectedCredits(64)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(1), ExpectedCredits(70)),
(Some(CommonResolution::FourEightyP), Seconds(12), Batch(1), ExpectedCredits(77)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(1), ExpectedCredits(83)),
(Some(CommonResolution::FourEightyP), Seconds(14), Batch(1), ExpectedCredits(90)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(1), ExpectedCredits(96)),
(Some(CommonResolution::SevenTwentyP), Seconds(4), Batch(1), ExpectedCredits(51)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(64)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(1), ExpectedCredits(76)),
@@ -97,19 +97,19 @@ mod seedance_2p0_fast {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(26)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(52)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(104)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(104)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(104)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(104)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(104)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(104)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(104)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(104)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(32)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(64)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(96)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(128)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(128)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(128)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(128)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(128)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(128)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(128)),
(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(255)),
(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)),
@@ -119,7 +119,7 @@ mod seedance_2p0_fast {
(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(255)),
(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)),
@@ -129,7 +129,7 @@ mod seedance_2p0_fast {
(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(255)),
(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)),
@@ -153,26 +153,26 @@ mod seedance_2p0_fast {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(62)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(145)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(187)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(228)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(269)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(311)),
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(77)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(134)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(230)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(282)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(333)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(384)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(153)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(356)),
(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(356)),
(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(356)),
(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)),
@@ -262,7 +262,7 @@ mod seedance_2p0_fast {
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(43)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(85)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(169)),
(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)),
@@ -272,7 +272,7 @@ mod seedance_2p0_fast {
(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(312)),
(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)),
@@ -282,7 +282,7 @@ mod seedance_2p0_fast {
(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(312)),
(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)),
@@ -292,7 +292,7 @@ mod seedance_2p0_fast {
(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(312)),
(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)),
@@ -311,6 +311,22 @@ 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).
/// Spot checks across duration x batch x resolution.
/// References must be CHARGED more than the identical no-reference
/// request, at every resolution and across durations.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_more_than_no_references() {
let harness = TestHarness::create().await;
for resolution in [CommonResolution::FourEightyP, CommonResolution::SevenTwentyP] {
for seconds in [5u16, 10, 15] {
assert_references_charge_more(
&harness, CommonVideoModel::Seedance2p0Fast, Some(resolution), Seconds(seconds),
).await;
}
}
}
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_spot_checked_combinations() {
@@ -318,25 +334,25 @@ mod seedance_2p0_fast {
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(101)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(236)),
(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(437)),
(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(437)),
(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(437)),
(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)),
@@ -433,7 +449,7 @@ mod seedance_2p0_bp_fast {
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(45)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(90)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(180)),
(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)),
@@ -443,7 +459,7 @@ mod seedance_2p0_bp_fast {
(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(400)),
(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)),
@@ -453,7 +469,7 @@ mod seedance_2p0_bp_fast {
(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(400)),
(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)),
@@ -463,7 +479,7 @@ mod seedance_2p0_bp_fast {
(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(400)),
(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)),
@@ -488,25 +504,25 @@ mod seedance_2p0_bp_fast {
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(108)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(252)),
(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(560)),
(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(560)),
(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(560)),
(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)),
@@ -528,54 +544,54 @@ mod seedance_2p0_bp_fast {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(4), Batch(1), ExpectedCredits(36)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(45)),
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(1), ExpectedCredits(54)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(1), ExpectedCredits(63)),
(Some(CommonResolution::FourEightyP), Seconds(8), Batch(1), ExpectedCredits(72)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(1), ExpectedCredits(81)),
(Some(CommonResolution::FourEightyP), Seconds(10), Batch(1), ExpectedCredits(90)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(1), ExpectedCredits(99)),
(Some(CommonResolution::FourEightyP), Seconds(12), Batch(1), ExpectedCredits(108)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(1), ExpectedCredits(117)),
(Some(CommonResolution::FourEightyP), Seconds(14), Batch(1), ExpectedCredits(126)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(1), ExpectedCredits(135)),
(Some(CommonResolution::SevenTwentyP), Seconds(4), Batch(1), ExpectedCredits(80)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(100)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(1), ExpectedCredits(120)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(1), ExpectedCredits(140)),
(Some(CommonResolution::SevenTwentyP), Seconds(8), Batch(1), ExpectedCredits(160)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(1), ExpectedCredits(180)),
(Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(200)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(1), ExpectedCredits(220)),
(Some(CommonResolution::SevenTwentyP), Seconds(12), Batch(1), ExpectedCredits(240)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(1), ExpectedCredits(260)),
(Some(CommonResolution::SevenTwentyP), Seconds(14), Batch(1), ExpectedCredits(280)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(300)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(80)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(100)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(120)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(140)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(160)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(180)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(200)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(220)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(240)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(260)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(280)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(300)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(80)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(100)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(120)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(140)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(160)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(180)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(200)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(220)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(240)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(260)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(280)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(300)),
(Some(CommonResolution::FourEightyP), Seconds(4), Batch(1), ExpectedCredits(39)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(49)),
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(1), ExpectedCredits(58)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(1), ExpectedCredits(68)),
(Some(CommonResolution::FourEightyP), Seconds(8), Batch(1), ExpectedCredits(78)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(1), ExpectedCredits(87)),
(Some(CommonResolution::FourEightyP), Seconds(10), Batch(1), ExpectedCredits(97)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(1), ExpectedCredits(107)),
(Some(CommonResolution::FourEightyP), Seconds(12), Batch(1), ExpectedCredits(116)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(1), ExpectedCredits(126)),
(Some(CommonResolution::FourEightyP), Seconds(14), Batch(1), ExpectedCredits(136)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(1), ExpectedCredits(145)),
(Some(CommonResolution::SevenTwentyP), Seconds(4), Batch(1), ExpectedCredits(82)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(103)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(1), ExpectedCredits(123)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(1), ExpectedCredits(144)),
(Some(CommonResolution::SevenTwentyP), Seconds(8), Batch(1), ExpectedCredits(164)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(1), ExpectedCredits(185)),
(Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(205)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(1), ExpectedCredits(226)),
(Some(CommonResolution::SevenTwentyP), Seconds(12), Batch(1), ExpectedCredits(246)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(1), ExpectedCredits(267)),
(Some(CommonResolution::SevenTwentyP), Seconds(14), Batch(1), ExpectedCredits(287)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(308)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(82)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(103)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(123)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(144)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(164)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(185)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(205)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(226)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(246)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(267)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(287)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(308)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(82)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(103)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(123)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(144)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(164)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(185)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(205)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(226)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(246)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(267)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(287)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(308)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -594,46 +610,46 @@ mod seedance_2p0_bp_fast {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(45)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(90)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(180)),
(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(400)),
(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(400)),
(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(400)),
(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)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(49)),
(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 {
@@ -645,36 +661,52 @@ 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).
/// Spot checks across duration x batch x resolution.
/// References must be CHARGED more than the identical no-reference
/// request, at every resolution and across durations.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_more_than_no_references() {
let harness = TestHarness::create().await;
for resolution in [CommonResolution::FourEightyP, CommonResolution::SevenTwentyP] {
for seconds in [5u16, 10, 15] {
assert_references_charge_more(
&harness, CommonVideoModel::Seedance2p0BytePlusFast, Some(resolution), Seconds(seconds),
).await;
}
}
}
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_spot_checked_combinations() {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(108)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(252)),
(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(560)),
(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(560)),
(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(560)),
(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)),
(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 {
@@ -783,7 +815,7 @@ mod seedance_2p0_bpu_fast {
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(45)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(90)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(180)),
(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)),
@@ -793,7 +825,7 @@ mod seedance_2p0_bpu_fast {
(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(400)),
(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)),
@@ -803,7 +835,7 @@ mod seedance_2p0_bpu_fast {
(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(400)),
(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)),
@@ -813,7 +845,7 @@ mod seedance_2p0_bpu_fast {
(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(400)),
(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)),
@@ -838,25 +870,25 @@ mod seedance_2p0_bpu_fast {
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(108)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(252)),
(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(560)),
(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(560)),
(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(560)),
(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)),
@@ -878,54 +910,54 @@ mod seedance_2p0_bpu_fast {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(4), Batch(1), ExpectedCredits(36)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(45)),
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(1), ExpectedCredits(54)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(1), ExpectedCredits(63)),
(Some(CommonResolution::FourEightyP), Seconds(8), Batch(1), ExpectedCredits(72)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(1), ExpectedCredits(81)),
(Some(CommonResolution::FourEightyP), Seconds(10), Batch(1), ExpectedCredits(90)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(1), ExpectedCredits(99)),
(Some(CommonResolution::FourEightyP), Seconds(12), Batch(1), ExpectedCredits(108)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(1), ExpectedCredits(117)),
(Some(CommonResolution::FourEightyP), Seconds(14), Batch(1), ExpectedCredits(126)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(1), ExpectedCredits(135)),
(Some(CommonResolution::SevenTwentyP), Seconds(4), Batch(1), ExpectedCredits(80)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(100)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(1), ExpectedCredits(120)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(1), ExpectedCredits(140)),
(Some(CommonResolution::SevenTwentyP), Seconds(8), Batch(1), ExpectedCredits(160)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(1), ExpectedCredits(180)),
(Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(200)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(1), ExpectedCredits(220)),
(Some(CommonResolution::SevenTwentyP), Seconds(12), Batch(1), ExpectedCredits(240)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(1), ExpectedCredits(260)),
(Some(CommonResolution::SevenTwentyP), Seconds(14), Batch(1), ExpectedCredits(280)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(300)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(80)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(100)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(120)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(140)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(160)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(180)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(200)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(220)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(240)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(260)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(280)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(300)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(80)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(100)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(120)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(140)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(160)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(180)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(200)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(220)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(240)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(260)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(280)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(300)),
(Some(CommonResolution::FourEightyP), Seconds(4), Batch(1), ExpectedCredits(39)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(49)),
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(1), ExpectedCredits(58)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(1), ExpectedCredits(68)),
(Some(CommonResolution::FourEightyP), Seconds(8), Batch(1), ExpectedCredits(78)),
(Some(CommonResolution::FourEightyP), Seconds(9), Batch(1), ExpectedCredits(87)),
(Some(CommonResolution::FourEightyP), Seconds(10), Batch(1), ExpectedCredits(97)),
(Some(CommonResolution::FourEightyP), Seconds(11), Batch(1), ExpectedCredits(107)),
(Some(CommonResolution::FourEightyP), Seconds(12), Batch(1), ExpectedCredits(116)),
(Some(CommonResolution::FourEightyP), Seconds(13), Batch(1), ExpectedCredits(126)),
(Some(CommonResolution::FourEightyP), Seconds(14), Batch(1), ExpectedCredits(136)),
(Some(CommonResolution::FourEightyP), Seconds(15), Batch(1), ExpectedCredits(145)),
(Some(CommonResolution::SevenTwentyP), Seconds(4), Batch(1), ExpectedCredits(82)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(103)),
(Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(1), ExpectedCredits(123)),
(Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(1), ExpectedCredits(144)),
(Some(CommonResolution::SevenTwentyP), Seconds(8), Batch(1), ExpectedCredits(164)),
(Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(1), ExpectedCredits(185)),
(Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(205)),
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(1), ExpectedCredits(226)),
(Some(CommonResolution::SevenTwentyP), Seconds(12), Batch(1), ExpectedCredits(246)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(1), ExpectedCredits(267)),
(Some(CommonResolution::SevenTwentyP), Seconds(14), Batch(1), ExpectedCredits(287)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(308)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(82)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(103)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(123)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(144)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(164)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(185)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(205)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(226)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(246)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(267)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(287)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(308)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(82)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(103)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(123)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(144)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(164)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(185)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(205)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(226)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(246)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(267)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(287)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(308)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -944,46 +976,46 @@ mod seedance_2p0_bpu_fast {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(45)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(90)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(180)),
(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(400)),
(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(400)),
(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(400)),
(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)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(49)),
(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 {
@@ -995,36 +1027,52 @@ 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).
/// Spot checks across duration x batch x resolution.
/// References must be CHARGED more than the identical no-reference
/// request, at every resolution and across durations.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_more_than_no_references() {
let harness = TestHarness::create().await;
for resolution in [CommonResolution::FourEightyP, CommonResolution::SevenTwentyP] {
for seconds in [5u16, 10, 15] {
assert_references_charge_more(
&harness, CommonVideoModel::Seedance2p0BytePlusUltraFast, Some(resolution), Seconds(seconds),
).await;
}
}
}
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn video_references_charge_spot_checked_combinations() {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits)] = &[
(Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(108)),
(Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(252)),
(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(560)),
(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(560)),
(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(560)),
(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)),
(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 {
@@ -1035,16 +1083,17 @@ mod seedance_2p0_bpu_fast {
}
}
// ── Seedance 2.0 Ultra Fast (GmiCloud, decommissioned) ──
// ── Seedance 2.0 Ultra Fast (deprecated, unroutable) ──
mod seedance_2p0_u_fast {
use super::*;
/// Seedance2p0UltraFast has no active execution route (its GmiCloud routing
/// is disabled in the pipeline). The request must fail cleanly BEFORE
/// billing. If the route is ever re-enabled, this pin fails and pricing
/// tests must be written for it.
/// Seedance2p0UltraFast is deprecated and has no execution route (its
/// GmiCloud routing was removed). The request must fail cleanly BEFORE
/// billing. If the model is ever revived, this pin fails and pricing tests
/// must be written for it.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
#[allow(deprecated)]
async fn seedance_2p0_ultra_fast_is_unroutable_and_charges_nothing() {
let harness = TestHarness::create().await;
@@ -1066,7 +1115,7 @@ mod premium {
let harness = TestHarness::create().await;
let cases: &[(Option<CommonResolution>, Seconds, Batch, ExpectedCredits, ExpectedCredits, CreditsDelta)] = &[
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(26), ExpectedCredits(45), CreditsDelta(19)),
(Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(32), ExpectedCredits(45), CreditsDelta(13)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(64), ExpectedCredits(100), CreditsDelta(36)),
(Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(127), ExpectedCredits(200), CreditsDelta(73)),
];
@@ -1081,25 +1130,28 @@ mod premium {
}
}
/// Seedance2p0UltraFast is NOT priced higher than Fast: its legacy (GmiCloud)
/// rate card quotes BELOW the Fast card (720p 5s: 45 vs 64), it has no active
/// execution route, and production has zero jobs for it, ever. Pinned via the
/// cost endpoint (it cannot generate); if this fails, someone revived or
/// repriced the Ultra tier and real pricing tests are required.
/// Seedance2p0UltraFast is deprecated: its router model, rate card, and
/// GmiCloud routing were removed, so the cost endpoint must REJECT it while
/// still quoting Fast normally (720p 5s: 64). Production has zero jobs for
/// it, ever. If the rejection stops, someone revived the Ultra tier and
/// real pricing tests are required.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn seedance_2p0_ultra_fast_quotes_below_fast_on_its_legacy_rate_card() {
#[allow(deprecated)]
async fn seedance_2p0_ultra_fast_is_deprecated_and_cannot_be_quoted() {
use actix_web::web::Json;
let fast_quote = quote_credits(CommonVideoModel::Seedance2p0Fast).await;
let ultra_fast_quote = quote_credits(CommonVideoModel::Seedance2p0UltraFast).await;
// 720p 5s: Fast 12.727 ¢/s → 64.
assert_eq!(quote_credits(CommonVideoModel::Seedance2p0Fast).await.expect("Fast quotes"), 64);
// 720p 5s: Fast 12.727 ¢/s → 64; UltraFast (legacy) 8.9089 ¢/s → 45.
assert_eq!(fast_quote, 64);
assert_eq!(ultra_fast_quote, 45);
assert_eq!(fast_quote - ultra_fast_quote, 19);
assert!(
quote_credits(CommonVideoModel::Seedance2p0UltraFast).await.is_err(),
"deprecated UltraFast must not quote",
);
async fn quote_credits(model: CommonVideoModel) -> u64 {
async fn quote_credits(
model: CommonVideoModel,
) -> Result<u64, crate::http_server::common_responses::common_web_error::CommonWebError> {
let mut request = crate::http_server::endpoints::omni_gen::generate::video::tests::support::base_generate_request(model);
request.resolution = Some(CommonResolution::SevenTwentyP);
request.duration_seconds = Some(5);
@@ -1108,14 +1160,11 @@ mod premium {
.uri("/v1/omni_gen/cost/video")
.to_http_request();
// No ServerState: quotes come from the router alone (no DB, no network).
crate::http_server::endpoints::omni_gen::cost::video::omni_gen_video_cost_handler::omni_gen_video_cost_handler(
let response = crate::http_server::endpoints::omni_gen::cost::video::omni_gen_video_cost_handler::omni_gen_video_cost_handler(
http_request, Json(request), None,
)
.await
.expect("cost quote should succeed")
.into_inner()
.cost_in_credits
.expect("quote should carry credits")
.await?;
Ok(response.into_inner().cost_in_credits.expect("quote should carry credits"))
}
}
}
@@ -4,6 +4,7 @@
//! with dummy Actix HTTP requests as a fixture user.
use std::collections::HashSet;
use std::convert::TryFrom;
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Duration;
@@ -794,6 +795,58 @@ pub async fn assert_generation_fails_and_charges_nothing(
);
}
/// Run the same generation twice — once without and once with a reference
/// video, as two independent fixture users — and assert the referenced
/// generation is CHARGED strictly more credits. The no-reference generation
/// succeeds outright; the referenced one uses an unreachable fixture (charge
/// then refund), so its charge is read from the ledger debit entry.
pub async fn assert_references_charge_more(
harness: &TestHarness,
model: CommonVideoModel,
resolution: Option<enums::common::generation::common_resolution::CommonResolution>,
Seconds(duration_seconds): Seconds,
) {
let no_ref_user = harness.create_funded_user(STARTING_CREDITS).await;
let mut request = base_generate_request(model);
request.resolution = resolution;
request.duration_seconds = Some(duration_seconds);
let response = harness
.post_generate(&no_ref_user, request)
.await
.unwrap_or_else(|err| {
panic!("{:?} {:?} {}s: no-reference generation failed: {:?}", model, resolution, duration_seconds, err)
});
assert!(response.success);
let no_ref_debit = STARTING_CREDITS - harness.wallet_balance(&no_ref_user).await;
let ref_user = harness.create_funded_user(STARTING_CREDITS).await;
let video_token = mysql_testing::fixtures::media_files::create_test_video_media_file(
&harness.pool,
&ref_user.user_token,
Some(6_000),
)
.await
.expect("create video media file fixture");
let mut request = base_generate_request(model);
request.resolution = resolution;
request.duration_seconds = Some(duration_seconds);
request.reference_video_media_tokens = Some(vec![video_token]);
// The unreachable fixture fails after billing; the charge is in the ledger.
let _ = harness.post_generate(&ref_user, request).await;
let entries = harness.ledger_entries(&ref_user).await;
let ref_debit = entries
.iter()
.find(|entry| entry.credits_delta < 0)
.unwrap_or_else(|| panic!("{:?} {:?} {}s: no ledger debit for referenced generation", model, resolution, duration_seconds));
let ref_debit = u64::try_from(-ref_debit.credits_delta).expect("positive debit");
assert!(
ref_debit > no_ref_debit,
"{:?} {:?} {}s: references must charge more (ref {} vs no-ref {})",
model, resolution, duration_seconds, ref_debit, no_ref_debit,
);
}
/// Assert the variant model charges a premium over the base model: runs one
/// generation on EACH model, asserts both exact charges, and that
/// `variant == base + delta`. (A delta of 0 is allowed where ceil-rounding
@@ -150,8 +150,6 @@ mod tests {
#[test]
fn ignores_over_limit_prompt_for_excluded_seedance_variants() {
let excluded_models = [
CommonVideoModel::Seedance2p0Ultra,
CommonVideoModel::Seedance2p0UltraFast,
CommonVideoModel::Seedance2p0BytePlusUltra,
CommonVideoModel::Seedance2p0BytePlusUltraFast,
CommonVideoModel::Seedance2p0BytePlusUltraMini,
@@ -103,6 +103,8 @@ pub fn model_family_for_type(
| Type::Kling3p0Standard
| Type::Kling3p0Pro => Family::Kling,
Type::HappyHorse1p0 => Family::HappyHorse,
// Historical rows may carry the deprecated Ultra tier variants.
#[allow(deprecated)]
Type::Seedance10Lite
| Type::Seedance1p5Pro
| Type::Seedance2p0
@@ -603,6 +603,10 @@ export default function CreateVideo() {
label: `@Image${i + 1}`,
type: "image" as const,
preview: img.url,
// `url` is the thumbnail — the chip Preview modal renders at
// natural size, so it needs the full-res image (same fallback
// the deck cards use).
fullPreview: img.fullUrl ?? img.url,
})),
...referenceVideos.map((vid, i) => ({
label: `@Video${i + 1}`,
@@ -23,8 +23,6 @@ export const MODEL_TYPE_GROUPS: SelectGroup[] = [
{ value: 'seedance_2p0_fast', label: 'Seedance 2.0 Fast' },
{ value: 'seedance_2p0_bp', label: 'Seedance 2.0 (BytePlus)' },
{ value: 'seedance_2p0_bp_fast', label: 'Seedance 2.0 Fast (BytePlus)' },
{ value: 'seedance_2p0_u', label: 'Seedance 2.0 Ultra' },
{ value: 'seedance_2p0_u_fast', label: 'Seedance 2.0 Ultra Fast' },
{ value: 'seedance_2p0_bpu', label: 'Seedance 2.0 Ultra (BytePlus)' },
{ value: 'seedance_2p0_bpu_fast', label: 'Seedance 2.0 Ultra Fast (BytePlus)' },
],
@@ -1,7 +1,14 @@
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { twMerge } from "tailwind-merge";
import { ChevronLeftIcon, EyeIcon, RefreshCwIcon, Trash2Icon, UserIcon } from "lucide-react";
import {
ChevronLeftIcon,
EyeIcon,
ImageIcon,
RefreshCwIcon,
Trash2Icon,
UserIcon,
} from "lucide-react";
import type { MentionItem } from "./MentionTextarea";
const VIEWPORT_MARGIN = 8;
@@ -15,6 +22,15 @@ const OPEN_GRACE_MS = 700;
const MENU_ROW =
"flex w-full items-center gap-2 rounded-md px-2 py-2 text-sm text-base-fg transition-colors hover:bg-ui-controls/60 cursor-pointer";
// Header subtitle and empty-replace-list copy per chip type.
const TYPE_COPY: Record<MentionItem["type"], { title: string; empty: string }> =
{
character: { title: "Character", empty: "No other characters" },
image: { title: "Image reference", empty: "No other images" },
video: { title: "Video reference", empty: "No other videos" },
audio: { title: "Audio reference", empty: "No other audio" },
};
export interface MentionChipMenuProps {
/** Viewport-space rect of the clicked chip. */
anchorRect: DOMRect;
@@ -26,9 +42,11 @@ export interface MentionChipMenuProps {
anchorNode?: HTMLElement;
/** Canonical mention label including the "@", e.g. "@robot cartoon". */
currentLabel: string;
/** Avatar of the currently attached character, shown in the menu header. */
/** Chip type — drives the header subtitle and fallback avatar icon. */
currentType?: MentionItem["type"];
/** Thumbnail of the current mention, shown in the menu header. */
currentPreview?: string;
/** Characters offered as replacements (current one excluded by the caller). */
/** Same-type mentions offered as replacements (current one excluded by the caller). */
replaceItems: MentionItem[];
onReplace: (item: MentionItem) => void;
onPreview: () => void;
@@ -37,9 +55,9 @@ export interface MentionChipMenuProps {
}
/**
* Floating menu for an inline character-mention chip: Replace / Preview /
* Remove, with the Replace action swapping to a second "Back" panel listing
* the other available characters.
* Floating menu for an inline mention chip (character or image ref):
* Replace / Preview / Remove, with the Replace action swapping to a second
* "Back" panel listing the other same-type mentions.
*
* Portaled to document.body with fixed positioning — the promptbox `.glass`
* container (backdrop-blur) is a containing block that would trap
@@ -49,6 +67,7 @@ export function MentionChipMenu({
anchorRect,
anchorNode,
currentLabel,
currentType = "character",
currentPreview,
replaceItems,
onReplace,
@@ -193,7 +212,11 @@ export function MentionChipMenu({
aria-label={`View ${currentName}`}
className="group/avatar relative shrink-0 cursor-pointer overflow-hidden rounded-md"
>
<ChipAvatar preview={currentPreview} name={currentName} />
<ChipAvatar
preview={currentPreview}
name={currentName}
type={currentType}
/>
<span className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity group-hover/avatar:opacity-100">
<EyeIcon className="h-3 w-3 text-white" />
</span>
@@ -202,7 +225,9 @@ export function MentionChipMenu({
<div className="truncate text-sm font-medium text-base-fg">
{currentName}
</div>
<div className="text-[11px] text-base-fg/50">Character</div>
<div className="text-[11px] text-base-fg/50">
{TYPE_COPY[currentType].title}
</div>
</div>
</div>
<div className="my-1 border-t border-ui-panel-border" />
@@ -247,7 +272,7 @@ export function MentionChipMenu({
<div className="max-h-64 overflow-y-auto">
{replaceItems.length === 0 && (
<div className="px-2 py-3 text-center text-xs text-base-fg/50">
No other characters
{TYPE_COPY[currentType].empty}
</div>
)}
{replaceItems.map((item, i) => (
@@ -258,7 +283,11 @@ export function MentionChipMenu({
onPointerDown={handleRowKeyActivate}
onClick={() => onReplace(item)}
>
<ChipAvatar preview={item.preview} name={item.label} />
<ChipAvatar
preview={item.preview}
name={item.label}
type={item.type}
/>
<span className="min-w-0 flex-1 truncate text-left">
{item.label.replace(/^@/, "")}
</span>
@@ -272,13 +301,22 @@ export function MentionChipMenu({
);
}
function ChipAvatar({ preview, name }: { preview?: string; name: string }) {
function ChipAvatar({
preview,
name,
type = "character",
}: {
preview?: string;
name: string;
type?: MentionItem["type"];
}) {
const FallbackIcon = type === "character" ? UserIcon : ImageIcon;
return (
<div className="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-md border border-white/10 bg-black/20">
{preview ? (
<img src={preview} alt={name} className="h-full w-full object-cover" />
) : (
<UserIcon className="h-3.5 w-3.5 text-base-fg/60" />
<FallbackIcon className="h-3.5 w-3.5 text-base-fg/60" />
)}
</div>
);
@@ -135,6 +135,15 @@ function isChip(node: Node): node is HTMLElement {
);
}
/**
* Mention types that render as atomic thumbnail chips (vs colored text).
* Video/audio refs stay as text: audio has no thumbnail, and a video frame
* shrunk to chip size is unreadable.
*/
function rendersAsChip(item: MentionItem): boolean {
return item.type === "character" || item.type === "image";
}
/** Nearest chip element enclosing `node` (up to `root`), or null. */
function chipContaining(root: HTMLElement, node: Node | null): HTMLElement | null {
let current: Node | null = node;
@@ -697,11 +706,11 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
return m;
}, [mentionItems]);
// Lowercased labels that render as atomic chips (characters only).
// Lowercased labels that render as atomic chips (characters + images).
const chipLabels = useMemo(() => {
const s = new Set<string>();
for (const item of mentionItems) {
if (item.type === "character") s.add(item.label.toLowerCase());
if (rendersAsChip(item)) s.add(item.label.toLowerCase());
}
return s;
}, [mentionItems]);
@@ -743,9 +752,9 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
);
/**
* Markup for one atomic character chip. data-mention keeps the exact
* typed text so serialization is lossless; the visible chip shows the
* name without the "@".
* Markup for one atomic mention chip (character or image ref).
* data-mention keeps the exact typed text so serialization is lossless;
* the visible chip shows the name without the "@".
*/
const buildChipHTML = useCallback(
(item: MentionItem, matchText: string): string => {
@@ -766,8 +775,8 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
[lowerColorMap],
);
// Build innerHTML with mentions inline: character mentions render as
// atomic thumbnail chips, other reference types as colored text.
// Build innerHTML with mentions inline: character and image-ref mentions
// render as atomic thumbnail chips, other reference types as colored text.
const buildHTML = useCallback(
(text: string): string => {
if (!text) return "";
@@ -794,7 +803,7 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
html += escapeHTML(text.slice(lastIndex, match.index));
}
if (item?.type === "character") {
if (item && rendersAsChip(item)) {
html += buildChipHTML(item, fullMatch);
endsWithChip = true;
} else if (color) {
@@ -1282,39 +1291,35 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
}
}, [chipMenu, selectChipRange, handleInput, chipStart, value, onChange]);
const handleChipPreview = useCallback(() => {
if (!chipMenu) return;
const item =
(chipMenu.token &&
mentionItems.find(
(i) => i.type === "character" && i.token === chipMenu.token,
)) ||
resolveItem(chipMenu.label);
setChipMenu(null);
if (item) setPreviewItem(item);
}, [chipMenu, mentionItems, resolveItem]);
/** The mention item the open chip menu refers to (token beats label). */
const chipMenuItem = useMemo(() => {
if (!chipMenu) return undefined;
return (
(chipMenu.token &&
mentionItems.find(
(i) => i.type === "character" && i.token === chipMenu.token,
)) ||
mentionItems.find((i) => i.token === chipMenu.token)) ||
resolveItem(chipMenu.label)
);
}, [chipMenu, mentionItems, resolveItem]);
const handleChipPreview = useCallback(() => {
if (!chipMenu) return;
setChipMenu(null);
if (chipMenuItem) setPreviewItem(chipMenuItem);
}, [chipMenu, chipMenuItem]);
// Swap candidates: other mentions of the same type as the clicked chip
// (characters swap with characters, image refs with image refs).
const replaceItems = useMemo(() => {
if (!chipMenu) return [];
const chipType = chipMenuItem?.type ?? "character";
return mentionItems.filter(
(i) =>
i.type === "character" &&
i.type === chipType &&
(chipMenu.token
? i.token !== chipMenu.token
: i.label.toLowerCase() !== chipMenu.label.toLowerCase()),
);
}, [chipMenu, mentionItems]);
}, [chipMenu, chipMenuItem, mentionItems]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
@@ -1355,8 +1360,8 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
// Atomic chip deletion: native contenteditable=false removal is
// inconsistent across browsers (Safari especially), so when the caret
// sits at a character-mention boundary, delete the whole label from
// the plain-text value in one keypress.
// sits at a chip-mention boundary, delete the whole label from the
// plain-text value in one keypress.
if (
(e.key === "Backspace" || e.key === "Delete") &&
chipLabels.size > 0 &&
@@ -1592,6 +1597,7 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
anchorRect={chipMenu.rect}
anchorNode={chipMenu.node}
currentLabel={chipMenu.label}
currentType={chipMenuItem?.type ?? "character"}
currentPreview={chipMenuItem ? chipMenuItem.preview : undefined}
replaceItems={replaceItems}
onReplace={handleChipReplace}
@@ -90,8 +90,6 @@ export const getModelDisplayName = (
seedance_2p0_bp: "Seedance 2.0 Plus",
seedance_2p0_bp_fast: "Seedance 2.0 Plus Fast",
seedance_2p0_bp_mini: "Seedance 2.0 Plus Mini",
seedance_2p0_u: "Seedance 2.0 Ultra",
seedance_2p0_u_fast: "Seedance 2.0 Ultra Fast",
seedance_2p0_bpu: "Seedance 2.0 Plus Ultra",
seedance_2p0_bpu_fast: "Seedance 2.0 Plus Ultra Fast",
seedance_2p0_bpu_mini: "Seedance 2.0 Plus Ultra Mini",
+56 -72
View File
@@ -1,106 +1,90 @@
#!/bin/bash
# NB: This format can be used without changing directory:
# SQLX_OFFLINE=true cargo sqlx prepare -- --bin storyteller-web --manifest-path crates/service/storyteller_web/Cargo.toml
#
# Regenerates the workspace-root `.sqlx/` offline query cache for BOTH query
# crates:
#
# 1. `sqlite_tasks` — desktop app "tasks" queries, checked against a scratch
# SQLite database migrated from `_database/sql/artcraft_migrations`.
# 2. `mysql_queries` — backend queries, checked against the local dev MySQL
# (the `DATABASE_URL` in the repo root `.env`, overridable via env).
#
# Requirements:
# - sqlx-cli matching the workspace sqlx version, with both drivers:
# cargo install sqlx-cli --version 0.7.4 --no-default-features \
# --features mysql,sqlite,rustls --locked
# - A running local MySQL with the migrated dev database.
#
# The new cache is staged in a temp directory and only replaces the old
# `.sqlx/*.json` files after BOTH prepares succeed, so a failure part-way
# never leaves the repo without a query cache.
set -euxo pipefail
# TODO: Fix up root dir determination
root_dir=$(pwd)
sqlite_db_file="/tmp/tasks.sqlite"
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
sqlite_db_file="/tmp/tasks.sqlite"
sqlite_package_path="${root_dir}/crates/schema/database/sqlite_tasks"
mysql_package_path="${root_dir}/crates/schema/database/mysql_queries"
query_cache_dir="${root_dir}/.sqlx/"
query_cache_dir="${root_dir}/.sqlx"
staging_dir="$(mktemp -d /tmp/sqlx_codegen.XXXXXX)"
remove_old_query_cache() {
echo "Removing old query cache files..."
pushd "${query_cache_dir}"
rm -f *.json
popd
}
# The dev MySQL database. Falls back to the DATABASE_URL in the repo root
# .env so the script works from any directory.
mysql_database_url="${DATABASE_URL:-$(grep -m1 '^DATABASE_URL=mysql' "${root_dir}/.env" | cut -d= -f2-)}"
prepare_sqlite_tasks() {
echo 'Create Tauri SQLite tasks database file...'
echo "Creating a fresh Tauri SQLite tasks database..."
rm -f "${sqlite_db_file}"
touch "${sqlite_db_file}"
echo "Migrate Tauri SQLite tasks database file..."
echo "Migrating the SQLite tasks database..."
cargo sqlx migrate run \
--database-url "sqlite:${sqlite_db_file}" \
--source "${root_dir}/_database/sql/artcraft_migrations"
echo "Prepare Tauri SQLite tasks query cache..."
echo "Preparing the SQLite tasks query cache..."
pushd "${sqlite_package_path}"
cargo sqlx prepare \
--database-url "sqlite:${sqlite_db_file}"
popd
echo "Move query cache files..."
pushd "${sqlite_package_path}/.sqlx"
mv *.json "${query_cache_dir}"
popd
mv "${sqlite_package_path}/.sqlx/"*.json "${staging_dir}/"
}
prepare_mysql() {
echo "Prepare MySQL database query cache..."
echo "Preparing the MySQL query cache..."
pushd "${mysql_package_path}"
cargo sqlx prepare
cargo sqlx prepare \
--database-url "${mysql_database_url}"
popd
echo "Move query cache files..."
pushd "${mysql_package_path}/.sqlx"
mv *.json "${query_cache_dir}"
popd
mv "${mysql_package_path}/.sqlx/"*.json "${staging_dir}/"
}
remove_old_query_cache
replace_query_cache() {
echo "Replacing the query cache..."
mkdir -p "${query_cache_dir}"
rm -f "${query_cache_dir}/"*.json
mv "${staging_dir}/"*.json "${query_cache_dir}/"
rmdir "${staging_dir}"
# Remove the (now empty) per-package cache dirs so sqlx never resolves
# offline queries against a stale crate-local cache instead of the
# workspace root one.
rmdir "${sqlite_package_path}/.sqlx" "${mysql_package_path}/.sqlx" 2>/dev/null || true
}
# The sqlx macros only emit query metadata when the crates actually
# recompile; a fresh (cached) build would yield an EMPTY cache. Force both
# query crates to rebuild.
cargo clean -p sqlite_tasks -p mysql_queries
# Prepare must expand the macros against the live databases.
export SQLX_OFFLINE=false
prepare_sqlite_tasks
prepare_mysql
# NB(bt): Below are several generational revisions of sqlx query caching.
# The most recent was for a workspace with only one sqlx query package.
# There are older remnants from a multi-package configuration, but I believe
# the tooling has improved since then. Keeping this here just in case.
#
# #export DATABASE_URL="sqlite:${sqlite_db_file}"
#
# # NB: Database used by desktop app for AiChatBot sidecar.
# # Make sure it has been migrated (use aichatbot-sidecar app directory as root).
# # SQLITE_DATABASE_PATH=./runtime_data/database.db
#
# # NB: OpenSSL is broken on Ubuntu 22.04 (this is documented elsewhere)
# # See https://askubuntu.com/a/1403961
# # For some reason, the choice of "rustls" or the vendored openssl is not persistent between calls.
# #cargo install sqlx-cli --features openssl-vendored,mysql
#
# build_shared_mysql_database_library() {
#
# # NB: For now, this is our monolithic DB that serves all of our microservices (gross)
#
# # It's a single package so that queries can be shared (again, gross)
# #pushd crates/lib/mysql_queries
# #SQLX_OFFLINE=true cargo sqlx prepare
# #popd
# cargo sqlx prepare --workspace
# }
#
# #build_shared_sqlite_database_library() {
# # # NB(1): These are *SQLite* queries for the desktop AiChatBot app.
# # # NB(2): Make sure to run migrations first.
#
# # # NB(3): SqLite has trouble with relative paths, so we copy the file to /
# # cp $SQLITE_DATABASE_PATH /tmp
# # pushd crates/lib/sqlite_queries
# # DATABASE_URL=sqlite:///tmp/database.db cargo sqlx prepare --merged
# # popd
# #}
#
#
# echo 'mysql prepare (if this takes a while, it may need to rebuild all the rust code)'
# build_shared_mysql_database_library
#
# #echo 'sqlite prepare'
# #build_shared_sqlite_database_library
replace_query_cache
echo 'done'