mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-29 13:02:51 -04:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfe71177ce | ||
|
|
43086c9071 | ||
|
|
f92e4f4836 | ||
|
|
be3f3ec5f9 | ||
|
|
5b4b392416 | ||
|
|
3c24333c7b | ||
|
|
88492018dc |
+18
-2
@@ -21,6 +21,8 @@ use crate::generate::generate_video::providers::artcraft::resolve::{
|
||||
pub enum SupportedResolutions {
|
||||
/// 480p, 720p, 1080p
|
||||
Full,
|
||||
/// 480p, 720p, 1080p, 4K
|
||||
FullWith4k,
|
||||
/// 480p, 720p only (1080p downgrades to 720p)
|
||||
Fast,
|
||||
/// 720p, 1080p only (480p upgrades)
|
||||
@@ -157,7 +159,7 @@ fn plan_output_resolution(
|
||||
None => Ok(None),
|
||||
|
||||
Some(RouterResolution::FourEightyP) => match supported {
|
||||
SupportedResolutions::Full | SupportedResolutions::Fast => {
|
||||
SupportedResolutions::Full | SupportedResolutions::FullWith4k | SupportedResolutions::Fast => {
|
||||
Ok(Some(CommonResolutionEnum::FourEightyP))
|
||||
}
|
||||
SupportedResolutions::NoFourEightyP => match strategy {
|
||||
@@ -175,7 +177,7 @@ fn plan_output_resolution(
|
||||
Some(RouterResolution::SevenTwentyP) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
|
||||
Some(RouterResolution::TenEightyP) => match supported {
|
||||
SupportedResolutions::Full | SupportedResolutions::NoFourEightyP => {
|
||||
SupportedResolutions::Full | SupportedResolutions::FullWith4k | SupportedResolutions::NoFourEightyP => {
|
||||
Ok(Some(CommonResolutionEnum::TenEightyP))
|
||||
}
|
||||
SupportedResolutions::Fast => match strategy {
|
||||
@@ -189,6 +191,20 @@ fn plan_output_resolution(
|
||||
},
|
||||
},
|
||||
|
||||
// 4K is only available on models flagged `FullWith4k` (the non-Fast Seedance
|
||||
// 2.0 family). Other models error out or fall back to their best resolution.
|
||||
Some(RouterResolution::FourK) => match (supported, strategy) {
|
||||
(SupportedResolutions::FullWith4k, _) => Ok(Some(CommonResolutionEnum::FourK)),
|
||||
(_, RequestMismatchMitigationStrategy::ErrorOut) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", RouterResolution::FourK),
|
||||
}))
|
||||
}
|
||||
(SupportedResolutions::Fast, _) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
(_, _) => Ok(Some(CommonResolutionEnum::TenEightyP)),
|
||||
},
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
pub(crate) mod build_common;
|
||||
pub(crate) mod seedance_common;
|
||||
#[cfg(test)]
|
||||
mod parity_tests;
|
||||
pub mod grok_imagine_video;
|
||||
|
||||
+7
-1
@@ -13,7 +13,7 @@ pub fn build_artcraft_seedance_2p0(builder: GenerateVideoRequestBuilder) -> Resu
|
||||
let request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0,
|
||||
SupportedResolutions::Full,
|
||||
SupportedResolutions::FullWith4k,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0RequestState { request };
|
||||
@@ -105,6 +105,12 @@ mod tests {
|
||||
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(|_| {}));
|
||||
|
||||
+62
-2
@@ -1,7 +1,8 @@
|
||||
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_common::seedance_2p0_four_k_usd_cents;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0::request::ArtcraftSeedance2p0RequestState;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
// ── Pricing constants ──
|
||||
//
|
||||
@@ -47,6 +48,23 @@ impl ArtcraftSeedance2p0CostState {
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -71,8 +89,8 @@ impl ArtcraftSeedance2p0CostState {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
// -- 720p pricing --
|
||||
@@ -157,6 +175,40 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -- 4K pricing --
|
||||
|
||||
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::ArtcraftSeedance2p0CostState {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Relative pricing --
|
||||
|
||||
mod relative_pricing_tests {
|
||||
@@ -171,6 +223,13 @@ mod tests {
|
||||
assert!(c720 < c1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_4k_more_expensive_than_1080p() {
|
||||
let c1080 = cost_cents(Some(RouterResolution::TenEightyP), 5, 1);
|
||||
let c4k = cost_cents(Some(RouterResolution::FourK), 5, 1);
|
||||
assert!(c4k > c1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c4 = cost_cents(Some(RouterResolution::SevenTwentyP), 4, 1);
|
||||
@@ -201,6 +260,7 @@ mod tests {
|
||||
Some(RouterResolution::FourEightyP),
|
||||
Some(RouterResolution::SevenTwentyP),
|
||||
Some(RouterResolution::TenEightyP),
|
||||
Some(RouterResolution::FourK),
|
||||
None,
|
||||
];
|
||||
for res in resolutions {
|
||||
|
||||
+7
-1
@@ -13,7 +13,7 @@ pub fn build_artcraft_seedance_2p0_bp(builder: GenerateVideoRequestBuilder) -> R
|
||||
let request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0BytePlus,
|
||||
SupportedResolutions::Full,
|
||||
SupportedResolutions::FullWith4k,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0BytePlusRequestState { request };
|
||||
@@ -101,6 +101,12 @@ mod tests {
|
||||
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(|_| {}));
|
||||
|
||||
+57
-3
@@ -1,7 +1,8 @@
|
||||
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_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;
|
||||
|
||||
/// USD cents per second by resolution:
|
||||
/// 480p: $0.10/s = 10.0 ¢/s
|
||||
@@ -15,6 +16,7 @@ pub struct ArtcraftSeedance2p0BytePlusCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
pub has_video_reference: bool,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0BytePlusCostState {
|
||||
@@ -23,10 +25,30 @@ impl ArtcraftSeedance2p0BytePlusCostState {
|
||||
.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.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,
|
||||
@@ -49,9 +71,9 @@ impl ArtcraftSeedance2p0BytePlusCostState {
|
||||
|
||||
#[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::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
mod pricing_720p {
|
||||
@@ -125,6 +147,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
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::ArtcraftSeedance2p0BytePlusCostState {
|
||||
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_tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
+7
-1
@@ -13,7 +13,7 @@ pub fn build_artcraft_seedance_2p0_bpu(builder: GenerateVideoRequestBuilder) ->
|
||||
let request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0BytePlusUltra,
|
||||
SupportedResolutions::Full,
|
||||
SupportedResolutions::FullWith4k,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0BytePlusUltraRequestState { request };
|
||||
@@ -101,6 +101,12 @@ mod tests {
|
||||
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(|_| {}));
|
||||
|
||||
+57
-3
@@ -1,7 +1,8 @@
|
||||
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_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;
|
||||
|
||||
/// USD cents per second by resolution:
|
||||
/// 480p: $0.10/s = 10.0 ¢/s
|
||||
@@ -15,6 +16,7 @@ pub struct ArtcraftSeedance2p0BytePlusUltraCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
pub has_video_reference: bool,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0BytePlusUltraCostState {
|
||||
@@ -23,10 +25,30 @@ impl ArtcraftSeedance2p0BytePlusUltraCostState {
|
||||
.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.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,
|
||||
@@ -49,9 +71,9 @@ impl ArtcraftSeedance2p0BytePlusUltraCostState {
|
||||
|
||||
#[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::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
mod pricing_720p {
|
||||
@@ -125,6 +147,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
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::ArtcraftSeedance2p0BytePlusUltraCostState {
|
||||
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_tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
+7
-1
@@ -13,7 +13,7 @@ pub fn build_artcraft_seedance_2p0_u(builder: GenerateVideoRequestBuilder) -> Re
|
||||
let request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0Ultra,
|
||||
SupportedResolutions::Full,
|
||||
SupportedResolutions::FullWith4k,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0UltraRequestState { request };
|
||||
@@ -105,6 +105,12 @@ mod tests {
|
||||
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(|_| {}));
|
||||
|
||||
+57
-3
@@ -1,7 +1,8 @@
|
||||
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_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 ──
|
||||
|
||||
@@ -13,6 +14,7 @@ pub struct ArtcraftSeedance2p0UltraCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
pub has_video_reference: bool,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0UltraCostState {
|
||||
@@ -21,11 +23,31 @@ impl ArtcraftSeedance2p0UltraCostState {
|
||||
.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 }
|
||||
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,
|
||||
@@ -49,9 +71,9 @@ impl ArtcraftSeedance2p0UltraCostState {
|
||||
|
||||
#[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::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
mod pricing_720p {
|
||||
@@ -122,6 +144,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
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::*;
|
||||
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
//! Shared pricing for the Seedance 2.0 family of ArtCraft providers.
|
||||
//!
|
||||
//! ArtCraft credits equal USD cents (100 credits = $1.00).
|
||||
|
||||
// 4K is priced uniformly across the non-Fast Seedance 2.0 models. Rates are held
|
||||
// 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;
|
||||
|
||||
/// 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 user-facing 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.
|
||||
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;
|
||||
|
||||
// Round up to whole cents.
|
||||
total_centi_cents.div_ceil(100)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
+14
-3
@@ -111,7 +111,7 @@ fn nearest_aspect_ratio(aspect_ratio: RouterAspectRatio) -> KinoviAspectRatio {
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.0 Pro supports output resolutions: 480p, 720p, 1080p.
|
||||
// Seedance 2.0 Pro supports output resolutions: 480p, 720p, 1080p, 4K.
|
||||
fn plan_output_resolution(
|
||||
resolution: Option<RouterResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
@@ -123,6 +123,7 @@ fn plan_output_resolution(
|
||||
Some(RouterResolution::FourEightyP) => Ok(Some(KinoviOutputResolution::FourEightyP)),
|
||||
Some(RouterResolution::SevenTwentyP) => Ok(Some(KinoviOutputResolution::SevenTwentyP)),
|
||||
Some(RouterResolution::TenEightyP) => Ok(Some(KinoviOutputResolution::TenEightyP)),
|
||||
Some(RouterResolution::FourK) => Ok(Some(KinoviOutputResolution::FourK)),
|
||||
|
||||
// Mismatches
|
||||
Some(unsupported) => match strategy {
|
||||
@@ -376,6 +377,16 @@ mod tests {
|
||||
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::TenEightyP)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_4k() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
resolution: Some(RouterResolution::FourK),
|
||||
..seedance2pro_builder()
|
||||
};
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
|
||||
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourK)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_none() {
|
||||
let builder = GenerateVideoRequestBuilder { resolution: None, ..seedance2pro_builder() };
|
||||
@@ -386,7 +397,7 @@ mod tests {
|
||||
#[test]
|
||||
fn unsupported_resolution_error_out() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
resolution: Some(RouterResolution::FourK),
|
||||
resolution: Some(RouterResolution::ThreeK),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..seedance2pro_builder()
|
||||
};
|
||||
@@ -396,7 +407,7 @@ mod tests {
|
||||
#[test]
|
||||
fn unsupported_resolution_rounds_up() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
resolution: Some(RouterResolution::FourK),
|
||||
resolution: Some(RouterResolution::ThreeK),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..seedance2pro_builder()
|
||||
};
|
||||
|
||||
+145
@@ -175,6 +175,121 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4K pricing ──
|
||||
//
|
||||
// Routes to the seedance2pro_client: 4K base is 200 credits/s (vs 90 at 1080p).
|
||||
// Credits → cents at 243 credits/$1, rounded up.
|
||||
|
||||
mod pricing_4k {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_4k_batch_1() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 4, KinoviBatchCount::One), 330);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::One), 412);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 10, KinoviBatchCount::One), 824);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 15, KinoviBatchCount::One), 1235);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_4k_batch_2() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::Two), 824);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_4k_batch_4() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::Four), 1647);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4K: router cost must match the seedance2pro_client binding ──
|
||||
//
|
||||
// The router delegates to GenerateSeedance2p0Request::calculate_costs(), so these
|
||||
// assert the exact 4K numbers AND cross-check the router against the binding for
|
||||
// every duration (4/5/10/15s) with and without a video reference.
|
||||
//
|
||||
// 4K base = 200 credits/s; with a video reference = 240 credits/s (+40 surcharge).
|
||||
// USD cents = ceil(total_credits / 243 * 100).
|
||||
|
||||
mod four_k_matches_binding {
|
||||
use super::*;
|
||||
use seedance2pro_client::generate::video::generate_seedance_2p0::GenerateSeedance2p0Request;
|
||||
|
||||
/// Router-side 4K cost (batch 1) for a duration / video-reference combo.
|
||||
fn router(duration_seconds: u8, has_video_reference: bool) -> VideoGenerationCostEstimate {
|
||||
KinoviSeedance2p0CostState {
|
||||
resolution: Some(KinoviOutputResolution::FourK),
|
||||
duration_seconds,
|
||||
batch_count: Some(KinoviBatchCount::One),
|
||||
has_video_reference,
|
||||
}
|
||||
.estimate_cost()
|
||||
}
|
||||
|
||||
/// The seedance2pro_client binding's own 4K cost for the same inputs.
|
||||
/// Returns (kinovi_credits, usd_cents_rounded_up).
|
||||
fn binding(duration_seconds: u8, has_video_reference: bool) -> (u64, u64) {
|
||||
let reference_video_urls = if has_video_reference {
|
||||
Some(vec!["pricing-placeholder".to_string()])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let costs = GenerateSeedance2p0Request {
|
||||
output_resolution: Some(KinoviOutputResolution::FourK),
|
||||
duration_seconds,
|
||||
batch_count: Some(KinoviBatchCount::One),
|
||||
reference_video_urls,
|
||||
prompt: String::new(),
|
||||
aspect_ratio: None,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
}
|
||||
.calculate_costs();
|
||||
(costs.total_cost.kinovi_credits, costs.total_cost.usd_cents_rounded_up)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_values_without_video_reference() {
|
||||
// 200 credits/s.
|
||||
assert_eq!((router(4, false).cost_in_credits, router(4, false).cost_in_usd_cents), (Some(800), Some(330)));
|
||||
assert_eq!((router(5, false).cost_in_credits, router(5, false).cost_in_usd_cents), (Some(1000), Some(412)));
|
||||
assert_eq!((router(10, false).cost_in_credits, router(10, false).cost_in_usd_cents), (Some(2000), Some(824)));
|
||||
assert_eq!((router(15, false).cost_in_credits, router(15, false).cost_in_usd_cents), (Some(3000), Some(1235)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_values_with_video_reference() {
|
||||
// 240 credits/s (200 base + 40 surcharge).
|
||||
assert_eq!((router(4, true).cost_in_credits, router(4, true).cost_in_usd_cents), (Some(960), Some(396)));
|
||||
assert_eq!((router(5, true).cost_in_credits, router(5, true).cost_in_usd_cents), (Some(1200), Some(494)));
|
||||
assert_eq!((router(10, true).cost_in_credits, router(10, true).cost_in_usd_cents), (Some(2400), Some(988)));
|
||||
assert_eq!((router(15, true).cost_in_credits, router(15, true).cost_in_usd_cents), (Some(3600), Some(1482)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_matches_binding_for_every_duration_and_video_ref() {
|
||||
for &duration in &[4u8, 5, 10, 15] {
|
||||
for &has_ref in &[false, true] {
|
||||
let r = router(duration, has_ref);
|
||||
let (b_credits, b_cents) = binding(duration, has_ref);
|
||||
assert_eq!(
|
||||
r.cost_in_credits, Some(b_credits),
|
||||
"credits differ at {duration}s has_video_reference={has_ref}",
|
||||
);
|
||||
assert_eq!(
|
||||
r.cost_in_usd_cents, Some(b_cents),
|
||||
"usd cents differ at {duration}s has_video_reference={has_ref}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Relative pricing ──
|
||||
|
||||
mod relative_pricing_tests {
|
||||
@@ -189,6 +304,13 @@ mod tests {
|
||||
assert!(c720 < c1080, "720p ({}) should be cheaper than 1080p ({})", c720, c1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_1080p_cheaper_than_4k() {
|
||||
let c1080 = usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One);
|
||||
let c4k = usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::One);
|
||||
assert!(c1080 < c4k, "1080p ({}) should be cheaper than 4K ({})", c1080, c4k);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c4 = usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One);
|
||||
@@ -306,6 +428,24 @@ mod tests {
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(31));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_draft_4k() {
|
||||
let draft = make_draft(5, 1, Some(RouterResolution::FourK), false);
|
||||
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
|
||||
assert!(matches!(cost.resolution, Some(KinoviOutputResolution::FourK)));
|
||||
assert_eq!(cost.estimate_cost().cost_in_credits, Some(1000));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(412));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_draft_4k_with_video_reference() {
|
||||
let draft = make_draft(5, 1, Some(RouterResolution::FourK), true);
|
||||
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
|
||||
// 4K base 200 + video-ref surcharge 40 = 240 credits/s -> 1200 credits for 5s.
|
||||
assert_eq!(cost.estimate_cost().cost_in_credits, Some(1200));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(494));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_draft_1080p_batch_4() {
|
||||
let draft = make_draft(5, 4, Some(RouterResolution::TenEightyP), false);
|
||||
@@ -388,6 +528,11 @@ mod tests {
|
||||
fn credits_1080p() {
|
||||
assert_eq!(credits(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One), 450);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credits_4k() {
|
||||
assert_eq!(credits(KinoviOutputResolution::FourK, 5, KinoviBatchCount::One), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
@@ -310,6 +310,10 @@ mod tests {
|
||||
request.calculate_costs().total_cost.kinovi_credits
|
||||
}
|
||||
|
||||
fn total_usd_cents(request: &GenerateSeedance2p0Request) -> u64 {
|
||||
request.calculate_costs().total_cost.usd_cents_rounded_up
|
||||
}
|
||||
|
||||
// ── 4K pricing (Seedance 2.0 only) ──
|
||||
//
|
||||
// Base: 200 credits/sec. With a video reference: 240 credits/sec
|
||||
@@ -363,6 +367,54 @@ mod tests {
|
||||
assert_eq!(total_credits(&r4k_with_video_ref(15)), 3600);
|
||||
}
|
||||
|
||||
// ── Total cost in USD cents — no video reference ──
|
||||
//
|
||||
// 200 credits/sec; usd_cents = ceil(total_credits / 243 * 100).
|
||||
|
||||
#[test]
|
||||
fn four_k_no_video_ref_4_seconds_is_330_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k(4)), 330);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_no_video_ref_5_seconds_is_412_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k(5)), 412);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_no_video_ref_10_seconds_is_824_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k(10)), 824);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_no_video_ref_15_seconds_is_1235_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k(15)), 1235);
|
||||
}
|
||||
|
||||
// ── Total cost in USD cents — with video reference ──
|
||||
//
|
||||
// 240 credits/sec (200 base + 40 surcharge); usd_cents = ceil(total_credits / 243 * 100).
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_ref_4_seconds_is_396_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k_with_video_ref(4)), 396);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_ref_5_seconds_is_494_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k_with_video_ref(5)), 494);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_ref_10_seconds_is_988_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k_with_video_ref(10)), 988);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_ref_15_seconds_is_1482_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k_with_video_ref(15)), 1482);
|
||||
}
|
||||
|
||||
// ── Base / surcharge breakdown (5 seconds) ──
|
||||
|
||||
#[test]
|
||||
|
||||
-391
@@ -105,90 +105,6 @@ impl std::fmt::Debug for WorkflowRunTaskArgs<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkflowRunTaskRequest {
|
||||
/// Estimates the credit cost for this generation request.
|
||||
///
|
||||
/// Pricing is per-second × batch count, with the per-second rate
|
||||
/// depending on model type and output resolution:
|
||||
///
|
||||
/// | Model | 480p | 720p | 1080p | 4K |
|
||||
/// |--------------|------|------|-------|-----|
|
||||
/// | Pro | 15 | 40 | 90 | 200 |
|
||||
/// | Fast | 10 | 28 | n/a | n/a |
|
||||
/// | HappyHorse | 15 | 40 | 90 | n/a |
|
||||
/// TODO(bt,2026-04-23): Not sure pricing for Happy Horse is correct here.
|
||||
/// NB: 4K is Seedance 2.0 (Pro) only; Fast does not support it. (This base-only
|
||||
/// estimate doesn't model the video-reference surcharge — see
|
||||
/// `generate_seedance_2p0::calculate_costs` for the authoritative cost.)
|
||||
///
|
||||
/// Input mode (text, keyframe, reference) has no effect on cost.
|
||||
/// Aspect ratio (`resolution` field) has no effect on cost.
|
||||
pub fn estimate_credits(&self) -> u32 {
|
||||
let credits_per_second: u32 = match (self.model_type, self.output_resolution) {
|
||||
// Seedance 2.0 Pro
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourEightyP)) => 15,
|
||||
(KinoviModelTypeRaw::Seedance2Pro, None)
|
||||
| (KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 40,
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::TenEightyP)) => 90,
|
||||
// 4K: 200 credits/sec (base). 4K is Seedance 2.0 (Pro) only.
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourK)) => 200,
|
||||
|
||||
// Seedance 2.0 Fast
|
||||
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourEightyP)) => 10,
|
||||
(KinoviModelTypeRaw::Seedance2Fast, None)
|
||||
| (KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 28,
|
||||
// NB: 1080p not officially supported for Fast, but price as 720p if requested
|
||||
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::TenEightyP)) => 28,
|
||||
// NB: 4K is NOT supported for Fast; price defensively at the 720p rate if ever requested.
|
||||
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourK)) => 28,
|
||||
|
||||
// TODO(bt,2026-04-23): Not sure pricing for Happy Horse is correct here.
|
||||
// Happy Horse 1.0 — same credit rates as Seedance 2.0 Pro
|
||||
(KinoviModelTypeRaw::HappyHorse1p0, Some(KinoviOutputResolutionRaw::FourEightyP)) => 15,
|
||||
(KinoviModelTypeRaw::HappyHorse1p0, None)
|
||||
| (KinoviModelTypeRaw::HappyHorse1p0, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 40,
|
||||
(KinoviModelTypeRaw::HappyHorse1p0, Some(KinoviOutputResolutionRaw::TenEightyP)) => 90,
|
||||
// NB: Happy Horse does NOT support 4K (only Seedance 2.0 Pro does); price
|
||||
// defensively at its 1080p rate if it's ever somehow requested.
|
||||
(KinoviModelTypeRaw::HappyHorse1p0, Some(KinoviOutputResolutionRaw::FourK)) => 90,
|
||||
};
|
||||
|
||||
let per_video = u32::from(self.duration_seconds) * credits_per_second;
|
||||
let batch_multiplier: u32 = match self.batch_count {
|
||||
KinoviBatchCountRaw::One => 1,
|
||||
KinoviBatchCountRaw::Two => 2,
|
||||
KinoviBatchCountRaw::Four => 4,
|
||||
};
|
||||
per_video * batch_multiplier
|
||||
}
|
||||
|
||||
/// Credits per dollar for billing conversion.
|
||||
///
|
||||
/// Legacy 720p pricing uses the original Kinovi credit package rates.
|
||||
/// All other model/resolution combos use the newer rate: 525,000 credits / $2,159.0909.
|
||||
fn credits_per_dollar(&self) -> f64 {
|
||||
match (self.model_type, self.output_resolution) {
|
||||
// Legacy: Seedance 2.0 Pro @ 720p — 25,000 credits for $99.99
|
||||
(KinoviModelTypeRaw::Seedance2Pro, None)
|
||||
| (KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 250.0,
|
||||
|
||||
// Legacy: Seedance 2.0 Fast @ 720p — 22,000 credits for $99.99
|
||||
(KinoviModelTypeRaw::Seedance2Fast, None)
|
||||
| (KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 220.0,
|
||||
|
||||
// New pricing: 525,000 credits for $2,159.0909 (~243.16 credits/$1)
|
||||
_ => 243.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
|
||||
let credits = self.estimate_credits() as f64;
|
||||
let credits_per_dollar = self.credits_per_dollar();
|
||||
let cost = credits / credits_per_dollar * 100.0;
|
||||
cost.round() as u64
|
||||
}
|
||||
}
|
||||
|
||||
// --- Public enums ---
|
||||
|
||||
/// Video resolution / aspect ratio.
|
||||
@@ -484,313 +400,6 @@ mod tests {
|
||||
use crate::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
|
||||
use crate::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
|
||||
|
||||
mod pricing_tests {
|
||||
use super::*;
|
||||
|
||||
fn make_args(
|
||||
model_type: KinoviModelTypeRaw,
|
||||
duration_seconds: u8,
|
||||
batch_count: KinoviBatchCountRaw,
|
||||
output_resolution: Option<KinoviOutputResolutionRaw>,
|
||||
) -> WorkflowRunTaskRequest {
|
||||
WorkflowRunTaskRequest {
|
||||
model_type,
|
||||
prompt: String::new(),
|
||||
aspect_ratio: KinoviAspectRatioRaw::Square1x1,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
output_resolution,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn pro(dur: u8, batch: KinoviBatchCountRaw) -> WorkflowRunTaskRequest {
|
||||
make_args(KinoviModelTypeRaw::Seedance2Pro, dur, batch, None)
|
||||
}
|
||||
|
||||
fn pro_res(dur: u8, batch: KinoviBatchCountRaw, res: KinoviOutputResolutionRaw) -> WorkflowRunTaskRequest {
|
||||
make_args(KinoviModelTypeRaw::Seedance2Pro, dur, batch, Some(res))
|
||||
}
|
||||
|
||||
fn fast(dur: u8, batch: KinoviBatchCountRaw) -> WorkflowRunTaskRequest {
|
||||
make_args(KinoviModelTypeRaw::Seedance2Fast, dur, batch, None)
|
||||
}
|
||||
|
||||
fn fast_res(dur: u8, batch: KinoviBatchCountRaw, res: KinoviOutputResolutionRaw) -> WorkflowRunTaskRequest {
|
||||
make_args(KinoviModelTypeRaw::Seedance2Fast, dur, batch, Some(res))
|
||||
}
|
||||
|
||||
// ── Spot checks: exact values from the pricing table ──
|
||||
|
||||
#[test]
|
||||
fn spot_check_pro_720p() {
|
||||
assert_eq!(pro(5, KinoviBatchCountRaw::One).estimate_credits(), 200);
|
||||
assert_eq!(pro(10, KinoviBatchCountRaw::One).estimate_credits(), 400);
|
||||
assert_eq!(pro(15, KinoviBatchCountRaw::One).estimate_credits(), 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_pro_480p() {
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 75);
|
||||
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 150);
|
||||
assert_eq!(pro_res(15, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 225);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_pro_1080p() {
|
||||
assert_eq!(pro_res(4, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 360);
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 450);
|
||||
assert_eq!(pro_res(6, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 540);
|
||||
assert_eq!(pro_res(7, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 630);
|
||||
assert_eq!(pro_res(8, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 720);
|
||||
assert_eq!(pro_res(9, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 810);
|
||||
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 900);
|
||||
assert_eq!(pro_res(11, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 990);
|
||||
assert_eq!(pro_res(12, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1080);
|
||||
assert_eq!(pro_res(13, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1170);
|
||||
assert_eq!(pro_res(14, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1260);
|
||||
assert_eq!(pro_res(15, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1350);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_pro_4k() {
|
||||
// 4K is Seedance 2.0 (Pro) only. Base rate: 200 credits/sec.
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourK).estimate_credits(), 1000);
|
||||
assert_eq!(pro_res(15, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourK).estimate_credits(), 3000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_4k_more_expensive_than_1080p() {
|
||||
let c1080 = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits();
|
||||
let c4k = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourK).estimate_credits();
|
||||
assert!(c4k > c1080, "4K ({}) should cost more than 1080p ({})", c4k, c1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_fast_720p() {
|
||||
assert_eq!(fast(5, KinoviBatchCountRaw::One).estimate_credits(), 140);
|
||||
assert_eq!(fast(10, KinoviBatchCountRaw::One).estimate_credits(), 280);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_fast_480p() {
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 50);
|
||||
assert_eq!(fast_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_more_expensive_than_fast_720p() {
|
||||
let p = pro(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
let f = fast(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
assert!(p > f, "Pro 720p ({}) should be more than Fast 720p ({})", p, f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_more_expensive_than_fast_480p() {
|
||||
let p = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits();
|
||||
let f = fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits();
|
||||
assert!(p > f, "Pro 480p ({}) should be more than Fast 480p ({})", p, f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_1080p_more_than_720p_more_than_480p() {
|
||||
let c480 = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits();
|
||||
let c720 = pro(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
let c1080 = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits();
|
||||
assert!(c480 < c720, "480p ({}) should be less than 720p ({})", c480, c720);
|
||||
assert!(c720 < c1080, "720p ({}) should be less than 1080p ({})", c720, c1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_720p_more_than_480p() {
|
||||
let c480 = fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits();
|
||||
let c720 = fast(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
assert!(c480 < c720, "Fast 480p ({}) should be less than Fast 720p ({})", c480, c720);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_none_same_as_720p() {
|
||||
let none = pro(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
let explicit = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).estimate_credits();
|
||||
assert_eq!(none, explicit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_none_same_as_720p() {
|
||||
let none = fast(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
let explicit = fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).estimate_credits();
|
||||
assert_eq!(none, explicit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_driven_credits() {
|
||||
let cases: Vec<(KinoviModelTypeRaw, Option<KinoviOutputResolutionRaw>, u8, KinoviBatchCountRaw, u32)> = vec![
|
||||
(KinoviModelTypeRaw::Seedance2Pro, None, 4, KinoviBatchCountRaw::One, 160),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, None, 5, KinoviBatchCountRaw::Two, 400),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, None, 10, KinoviBatchCountRaw::Four, 1600),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourEightyP), 5, KinoviBatchCountRaw::One, 75),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourEightyP), 10, KinoviBatchCountRaw::Two, 300),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourEightyP), 15, KinoviBatchCountRaw::Four, 900),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::TenEightyP), 5, KinoviBatchCountRaw::One, 450),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::TenEightyP), 10, KinoviBatchCountRaw::Two, 1800),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::TenEightyP), 15, KinoviBatchCountRaw::Four, 5400),
|
||||
(KinoviModelTypeRaw::Seedance2Fast, None, 4, KinoviBatchCountRaw::One, 112),
|
||||
(KinoviModelTypeRaw::Seedance2Fast, None, 5, KinoviBatchCountRaw::Two, 280),
|
||||
(KinoviModelTypeRaw::Seedance2Fast, None, 15, KinoviBatchCountRaw::Four, 1680),
|
||||
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourEightyP), 5, KinoviBatchCountRaw::One, 50),
|
||||
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourEightyP), 10, KinoviBatchCountRaw::Two, 200),
|
||||
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourEightyP), 15, KinoviBatchCountRaw::Four, 600),
|
||||
];
|
||||
|
||||
for (i, (model, res, dur, batch, expected)) in cases.iter().enumerate() {
|
||||
let args = make_args(*model, *dur, *batch, *res);
|
||||
let actual = args.estimate_credits();
|
||||
assert_eq!(
|
||||
actual, *expected,
|
||||
"Case {}: {:?} {:?} {}s batch {:?} — expected {} credits, got {}",
|
||||
i, model, res, dur, batch, expected, actual,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_does_not_affect_credits() {
|
||||
let resolutions = [
|
||||
KinoviAspectRatioRaw::Landscape16x9,
|
||||
KinoviAspectRatioRaw::Portrait9x16,
|
||||
KinoviAspectRatioRaw::Square1x1,
|
||||
KinoviAspectRatioRaw::Landscape4x3,
|
||||
KinoviAspectRatioRaw::Portrait3x4,
|
||||
];
|
||||
|
||||
let baseline = pro(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
|
||||
for res in &resolutions {
|
||||
let req = WorkflowRunTaskRequest {
|
||||
model_type: KinoviModelTypeRaw::Seedance2Pro,
|
||||
prompt: String::new(),
|
||||
aspect_ratio: *res,
|
||||
duration_seconds: 5,
|
||||
batch_count: KinoviBatchCountRaw::One,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
output_resolution: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
};
|
||||
assert_eq!(
|
||||
req.estimate_credits(), baseline,
|
||||
"Aspect ratio {:?} should not change credits from baseline {}",
|
||||
res, baseline,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_legacy_pro_720p() {
|
||||
assert_eq!(pro(5, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 80);
|
||||
assert_eq!(pro(10, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 160);
|
||||
assert_eq!(pro(15, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 240);
|
||||
assert_eq!(pro(5, KinoviBatchCountRaw::Two).estimate_cost_in_usd_cents(), 160);
|
||||
assert_eq!(pro(5, KinoviBatchCountRaw::Four).estimate_cost_in_usd_cents(), 320);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_legacy_fast_720p() {
|
||||
assert_eq!(fast(5, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 64);
|
||||
assert_eq!(fast(10, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 127);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_new_pro_480p() {
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 31);
|
||||
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 62);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_new_pro_1080p() {
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 185);
|
||||
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 370);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_new_fast_480p() {
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 21);
|
||||
assert_eq!(fast_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 41);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credits_per_dollar_legacy_rates() {
|
||||
assert_eq!(pro(5, KinoviBatchCountRaw::One).credits_per_dollar(), 250.0);
|
||||
assert_eq!(fast(5, KinoviBatchCountRaw::One).credits_per_dollar(), 220.0);
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).credits_per_dollar(), 250.0);
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).credits_per_dollar(), 220.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credits_per_dollar_new_rate() {
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 243.0);
|
||||
}
|
||||
|
||||
// ── Happy Horse 1.0 ──
|
||||
|
||||
fn happy_horse(dur: u8, batch: KinoviBatchCountRaw) -> WorkflowRunTaskRequest {
|
||||
make_args(KinoviModelTypeRaw::HappyHorse1p0, dur, batch, None)
|
||||
}
|
||||
|
||||
fn happy_horse_res(dur: u8, batch: KinoviBatchCountRaw, res: KinoviOutputResolutionRaw) -> WorkflowRunTaskRequest {
|
||||
make_args(KinoviModelTypeRaw::HappyHorse1p0, dur, batch, Some(res))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_happy_horse_720p() {
|
||||
// Same as Pro: 40 credits/sec
|
||||
assert_eq!(happy_horse(5, KinoviBatchCountRaw::One).estimate_credits(), 200);
|
||||
assert_eq!(happy_horse(10, KinoviBatchCountRaw::One).estimate_credits(), 400);
|
||||
assert_eq!(happy_horse(15, KinoviBatchCountRaw::One).estimate_credits(), 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_happy_horse_480p() {
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 75);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_happy_horse_1080p() {
|
||||
assert_eq!(happy_horse_res(4, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 360);
|
||||
assert_eq!(happy_horse_res(15, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1350);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn happy_horse_uses_new_pricing_rate() {
|
||||
// All Happy Horse resolutions use the new 243 credits/$1 rate (not legacy)
|
||||
assert_eq!(happy_horse(5, KinoviBatchCountRaw::One).credits_per_dollar(), 243.0);
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 243.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn happy_horse_usd_cents_1080p() {
|
||||
// 243 credits/$1: 360 credits (4s×90) = 148.15 → 148¢
|
||||
assert_eq!(happy_horse_res(4, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 148);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bitrate serialization ──
|
||||
//
|
||||
// The optional `bitrate_mode` field is sent only when `High` is requested;
|
||||
|
||||
+3
@@ -43,6 +43,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
CommonResolution::FourK,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
@@ -136,6 +137,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
CommonResolution::FourK,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
@@ -229,6 +231,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
CommonResolution::FourK,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
|
||||
Reference in New Issue
Block a user