From 7f0154efe490ed7e700f511ee3b14d64b76a4460 Mon Sep 17 00:00:00 2001 From: Hanashi Date: Fri, 7 Aug 2026 23:08:03 -0400 Subject: [PATCH] clamp seconds --- .../providers/artcraft/seedance_2p5/cost.rs | 20 +++++++++++-- .../providers/kinovi/seedance_2p5/cost.rs | 22 ++++++++++++-- .../generate/video/generate_seedance_2p5.rs | 29 +++++++++++++++---- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5/cost.rs index 76870b30fa..b9470e7497 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p5/cost.rs @@ -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 { diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p5/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p5/cost.rs index 2b7dc22d51..f101f14aaf 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p5/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p5/cost.rs @@ -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, 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) ── diff --git a/crates/api_clients/kinovi_web_client/src/generate/video/generate_seedance_2p5.rs b/crates/api_clients/kinovi_web_client/src/generate/video/generate_seedance_2p5.rs index 8b24a89b0e..be0aca2a0c 100644 --- a/crates/api_clients/kinovi_web_client/src/generate/video/generate_seedance_2p5.rs +++ b/crates/api_clients/kinovi_web_client/src/generate/video/generate_seedance_2p5.rs @@ -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));