clamp seconds

This commit is contained in:
Hanashi committed 2026-08-07 23:08:03 -04:00
1 parent 845de872ca
commit 7f0154efe4
3 files changed
+62 -9

No files matched your search

@@ -1,4 +1,5 @@
use enums::common::generation::common_resolution::CommonResolution;
use kinovi_web_client::generate::video::generate_seedance_2p5::MAX_BILLED_INPUT_SECONDS;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p5_usd_cents;
@@ -7,7 +8,9 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p5::request:
/// Seedance 2.5 pricing depends on the resolution, the output duration, and
/// — when reference videos are attached — the total seconds of reference
/// video input, which are billed on top of the output duration (at a lower
/// per-second rate). No batching.
/// per-second rate). Input seconds are clamped to
/// [`MAX_BILLED_INPUT_SECONDS`] — the model accepts at most 30 seconds of
/// video. No batching.
pub struct ArtcraftSeedance2p5CostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
@@ -34,11 +37,14 @@ impl ArtcraftSeedance2p5CostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
let total_input_seconds = self.total_input_seconds
.min(u16::from(MAX_BILLED_INPUT_SECONDS));
let usd_cents = seedance_2p5_usd_cents(
self.resolution,
self.duration_seconds,
self.has_video_references,
self.total_input_seconds,
total_input_seconds,
);
// ArtCraft credits: 100 credits = $1.00, so credits = cents.
@@ -114,6 +120,16 @@ mod tests {
// 7.24279835 ¢/s × 10 = 72.43 → 73¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 10, None), 73);
}
#[test]
fn input_seconds_clamp_to_max_billed_input_seconds() {
// 200 input seconds clamp to 30: 7.24279835 ¢/s × (30+30) = 434.57 → 435¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(200)), 435);
assert_eq!(
cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(200)),
cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(30)),
);
}
}
mod credits_tests {
@@ -1,5 +1,6 @@
use kinovi_web_client::generate::video::generate_seedance_2p5::{
GenerateSeedance2p5Request, KinoviSeedance2p5Modality, KinoviSeedance2p5OutputResolution,
MAX_BILLED_INPUT_SECONDS,
};
use crate::api::video_list_ref::VideoListRef;
@@ -10,7 +11,9 @@ use crate::generate::generate_video::providers::kinovi::seedance_2p5::request::K
/// Seedance 2.5 pricing depends on the resolution, the output duration, and
/// — when reference videos are attached — the total seconds of reference
/// video input, which are billed on top of the output duration (at a lower
/// per-second rate).
/// per-second rate). Input seconds are clamped to
/// [`MAX_BILLED_INPUT_SECONDS`] — the model accepts at most 30 seconds of
/// video.
pub struct KinoviSeedance2p5CostState {
pub resolution: Option<KinoviSeedance2p5OutputResolution>,
pub duration_seconds: u8,
@@ -70,12 +73,17 @@ impl KinoviSeedance2p5CostState {
}
};
// NB: The kinovi calculator clamps too; clamping here as well keeps this
// state's math correct even if it's read directly.
let total_input_seconds = self.total_input_seconds
.map(|seconds| seconds.min(MAX_BILLED_INPUT_SECONDS));
let pricing_request = GenerateSeedance2p5Request {
prompt: String::new(),
modality,
output_resolution: self.resolution,
duration_seconds: self.duration_seconds,
total_input_seconds: self.total_input_seconds,
total_input_seconds,
use_face_blur_hack: None,
};
@@ -153,6 +161,16 @@ mod tests {
fn missing_input_seconds_bill_output_duration_only() {
assert_eq!(credits(Some(KinoviOutputResolution::FourEightyP), 10, true, None), 160);
}
#[test]
fn input_seconds_clamp_to_max_billed_input_seconds() {
// 200 input seconds clamp to 30: 30s output + 30 = 60 billed seconds.
assert_eq!(credits(Some(KinoviOutputResolution::FourEightyP), 30, true, Some(200)), 16 * 60);
assert_eq!(
credits(Some(KinoviOutputResolution::FourEightyP), 30, true, Some(200)),
credits(Some(KinoviOutputResolution::FourEightyP), 30, true, Some(30)),
);
}
}
// ── USD cents (rounded up) ──
@@ -7,6 +7,12 @@ use crate::requests::workflow_run_task::workflow_run_task::{
KinoviOutputResolutionRaw, WorkflowRunTaskArgs, WorkflowRunTaskRequest,
};
// ── Constants ──
/// Seedance 2.5 supports at most 30 seconds of video: reference-video input
/// seconds beyond this are clamped for billing.
pub const MAX_BILLED_INPUT_SECONDS: u8 = 30;
// ── Args ──
pub struct GenerateSeedance2p5Args<'a> {
@@ -111,9 +117,10 @@ pub enum KinoviSeedance2p5OutputResolution {
//
// With video references, the rate drops but the billed seconds are the
// output duration PLUS the total seconds of reference video input
// (`total_input_seconds`, summed across all reference videos). E.g. a 30s
// output with a 10s video reference bills as 40 seconds; a 30s output with
// two 7s references (14s total input) bills as 44 seconds.
// (`total_input_seconds`, summed across all reference videos and clamped to
// [`MAX_BILLED_INPUT_SECONDS`]). E.g. a 30s output with a 10s video
// reference bills as 40 seconds; a 30s output with two 7s references (14s
// total input) bills as 44 seconds.
//
// Default resolution (None) is 720p.
// Credit package: 525,000 credits for $2,159.0909 (~243 credits/$1).
@@ -132,8 +139,10 @@ impl GenerateSeedance2p5Request {
Some(KinoviSeedance2p5OutputResolution::FourEightyP) => 16u64,
Some(KinoviSeedance2p5OutputResolution::SevenTwentyP) | None => 35u64,
};
let seconds = u64::from(self.duration_seconds)
+ u64::from(self.total_input_seconds.unwrap_or(0));
let input_seconds = self.total_input_seconds
.unwrap_or(MAX_BILLED_INPUT_SECONDS)
.min(MAX_BILLED_INPUT_SECONDS);
let seconds = u64::from(self.duration_seconds) + u64::from(input_seconds);
(rate, seconds)
} else {
let rate = match self.output_resolution {
@@ -389,6 +398,16 @@ mod tests {
assert_eq!(video_ref_720(10, None).calculate_costs().kinovi_credits, 350.0);
}
#[test]
fn input_seconds_clamp_to_max_billed_input_seconds() {
// 200 input seconds clamp to 30: 30s output + 30 = 60 billed seconds.
assert_eq!(video_ref_480(30, Some(200)).calculate_costs().kinovi_credits, (16 * 60) as f64);
assert_eq!(
video_ref_480(30, Some(200)).calculate_costs(),
video_ref_480(30, Some(MAX_BILLED_INPUT_SECONDS)).calculate_costs(),
);
}
#[test]
fn mixed_references_still_use_video_reference_rates() {
let mut request = video_ref_480(10, Some(5));