diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/kinovi/happy_horse_1p0/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/kinovi/happy_horse_1p0/cost.rs index c8c9ba043c..5796144e52 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/kinovi/happy_horse_1p0/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/kinovi/happy_horse_1p0/cost.rs @@ -1,6 +1,6 @@ -use seedance2pro_client::requests::workflow_run_task::workflow_run_task::{ - KinoviAspectRatioRaw, KinoviBatchCountRaw, KinoviModelTypeRaw, - KinoviOutputResolutionRaw, WorkflowRunTaskRequest, +use seedance2pro_client::generate::video::generate_happy_horse_1p0::{ + GenerateHappyHorse1p0Request, KinoviHappyHorse1p0BatchCount, + KinoviHappyHorse1p0OutputResolution, }; use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; @@ -8,49 +8,42 @@ use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::draf use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::request::KinoviHappyHorse1p0RequestState; pub struct KinoviHappyHorse1p0CostState { - pub resolution: Option, + pub resolution: Option, pub duration_seconds: u8, - pub batch_count: KinoviBatchCountRaw, + pub batch_count: Option, } impl KinoviHappyHorse1p0CostState { pub fn from_request(request: &KinoviHappyHorse1p0RequestState) -> Self { Self { - resolution: request.request.output_resolution.map(map_output_resolution), + resolution: request.request.output_resolution, duration_seconds: request.request.duration_seconds, - batch_count: map_batch_count(request.request.batch_count), + batch_count: request.request.batch_count, } } pub fn from_draft(draft: &KinoviHappyHorse1p0DraftState) -> Self { Self { - resolution: draft.resolution.map(map_output_resolution), + resolution: draft.resolution, duration_seconds: draft.duration_seconds, - batch_count: map_batch_count(draft.batch_count), + batch_count: draft.batch_count, } } pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { - let raw_request = WorkflowRunTaskRequest { - model_type: KinoviModelTypeRaw::HappyHorse1p0, + let pricing_request = GenerateHappyHorse1p0Request { output_resolution: self.resolution, duration_seconds: self.duration_seconds, batch_count: self.batch_count, // No impact on price prompt: String::new(), - aspect_ratio: KinoviAspectRatioRaw::Landscape16x9, + aspect_ratio: None, start_frame_url: None, - end_frame_url: None, - reference_image_urls: None, - reference_video_urls: None, - reference_audio_urls: None, - character_ids: None, - use_face_blur_hack: None, }; - let cost_in_credits = raw_request.estimate_credits(); - let cost_in_usd_cents = raw_request.estimate_cost_in_usd_cents(); + let cost_in_credits = pricing_request.estimate_credits(); + let cost_in_usd_cents = pricing_request.estimate_cost_in_usd_cents(); VideoGenerationCostEstimate { cost_in_credits: Some(cost_in_credits as u64), @@ -63,27 +56,6 @@ impl KinoviHappyHorse1p0CostState { } } -// ── Mapping helpers ── - -use seedance2pro_client::generate::video::generate_happy_horse_1p0::{ - KinoviHappyHorse1p0BatchCount, KinoviHappyHorse1p0OutputResolution, -}; - -fn map_output_resolution(res: KinoviHappyHorse1p0OutputResolution) -> KinoviOutputResolutionRaw { - match res { - KinoviHappyHorse1p0OutputResolution::SevenTwentyP => KinoviOutputResolutionRaw::SevenTwentyP, - KinoviHappyHorse1p0OutputResolution::TenEightyP => KinoviOutputResolutionRaw::TenEightyP, - } -} - -fn map_batch_count(bc: Option) -> KinoviBatchCountRaw { - match bc { - None | Some(KinoviHappyHorse1p0BatchCount::One) => KinoviBatchCountRaw::One, - Some(KinoviHappyHorse1p0BatchCount::Two) => KinoviBatchCountRaw::Two, - Some(KinoviHappyHorse1p0BatchCount::Four) => KinoviBatchCountRaw::Four, - } -} - #[cfg(test)] mod tests { use super::*; @@ -93,58 +65,102 @@ mod tests { use crate::generate::generate_video_v2::video_generation_draft::VideoGenerationDraftRequest; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; - // ── 720p pricing ── + // ── 720p pricing (33 credits/sec) ── mod pricing_720p { use super::*; #[test] - fn cost_720p_batch_1() { - assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 4, KinoviBatchCountRaw::One), 83); - assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 5, KinoviBatchCountRaw::One), 104); - assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 10, KinoviBatchCountRaw::One), 207); - assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 15, KinoviBatchCountRaw::One), 311); + fn credits_720p_every_duration() { + assert_eq!(credits(None, 3), 99); + assert_eq!(credits(None, 4), 132); + assert_eq!(credits(None, 5), 165); + assert_eq!(credits(None, 6), 198); + assert_eq!(credits(None, 7), 231); + assert_eq!(credits(None, 8), 264); + assert_eq!(credits(None, 9), 297); + assert_eq!(credits(None, 10), 330); + assert_eq!(credits(None, 11), 363); + assert_eq!(credits(None, 12), 396); + assert_eq!(credits(None, 13), 429); + assert_eq!(credits(None, 14), 462); + assert_eq!(credits(None, 15), 495); } #[test] - fn cost_720p_batch_2() { - assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 5, KinoviBatchCountRaw::Two), 207); - } - - #[test] - fn cost_720p_batch_4() { - assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 5, KinoviBatchCountRaw::Four), 415); - } - - #[test] - fn default_resolution_same_as_720p() { - let default = usd_cents(None, 5, KinoviBatchCountRaw::One); - let explicit = usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 5, KinoviBatchCountRaw::One); + fn explicit_720p_same_as_default() { + let default = credits(None, 5); + let explicit = credits(Some(KinoviHappyHorse1p0OutputResolution::SevenTwentyP), 5); assert_eq!(default, explicit); } + + #[test] + fn usd_cents_720p() { + // 165 credits / 193 * 100 = 85.49 → 85¢ + assert_eq!(usd_cents(None, 5, None), 85); + // 495 credits / 193 * 100 = 256.48 → 256¢ + assert_eq!(usd_cents(None, 15, None), 256); + } } - // ── 1080p pricing ── + // ── 1080p pricing (66 credits/sec) ── mod pricing_1080p { use super::*; #[test] - fn cost_1080p_batch_1() { - assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 4, KinoviBatchCountRaw::One), 187); - assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 5, KinoviBatchCountRaw::One), 233); - assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 10, KinoviBatchCountRaw::One), 466); - assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 15, KinoviBatchCountRaw::One), 699); + fn credits_1080p_every_duration() { + let r = Some(KinoviHappyHorse1p0OutputResolution::TenEightyP); + assert_eq!(credits(r, 3), 198); + assert_eq!(credits(r, 4), 264); + assert_eq!(credits(r, 5), 330); + assert_eq!(credits(r, 6), 396); + assert_eq!(credits(r, 7), 462); + assert_eq!(credits(r, 8), 528); + assert_eq!(credits(r, 9), 594); + assert_eq!(credits(r, 10), 660); + assert_eq!(credits(r, 11), 726); + assert_eq!(credits(r, 12), 792); + assert_eq!(credits(r, 13), 858); + assert_eq!(credits(r, 14), 924); + assert_eq!(credits(r, 15), 990); } #[test] - fn cost_1080p_batch_2() { - assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 5, KinoviBatchCountRaw::Two), 466); + fn usd_cents_1080p() { + let r = Some(KinoviHappyHorse1p0OutputResolution::TenEightyP); + // 330 credits / 193 * 100 = 170.98 → 171¢ + assert_eq!(usd_cents(r, 5, None), 171); + // 990 credits / 193 * 100 = 512.95 → 513¢ + assert_eq!(usd_cents(r, 15, None), 513); + } + } + + // ── Batch multiplier ── + + mod batch_tests { + use super::*; + + #[test] + fn batch_2_doubles_credits() { + let base = credits(None, 5); + let batch2 = credits_with_batch(None, 5, Some(KinoviHappyHorse1p0BatchCount::Two)); + assert_eq!(batch2, base * 2); } #[test] - fn cost_1080p_batch_4() { - assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 5, KinoviBatchCountRaw::Four), 933); + fn batch_4_quadruples_credits() { + let base = credits(None, 5); + let batch4 = credits_with_batch(None, 5, Some(KinoviHappyHorse1p0BatchCount::Four)); + assert_eq!(batch4, base * 4); + } + + #[test] + fn batch_applies_to_1080p() { + let r = Some(KinoviHappyHorse1p0OutputResolution::TenEightyP); + let base = credits(r, 5); + let batch4 = credits_with_batch(r, 5, Some(KinoviHappyHorse1p0BatchCount::Four)); + assert_eq!(batch4, base * 4); } } @@ -154,40 +170,33 @@ mod tests { use super::*; #[test] - fn cost_720p_cheaper_than_1080p() { - let c720 = usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 5, KinoviBatchCountRaw::One); - let c1080 = usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 5, KinoviBatchCountRaw::One); - assert!(c720 < c1080, "720p ({}) should be cheaper than 1080p ({})", c720, c1080); + fn r1080p_is_double_720p() { + for dur in 3..=15u8 { + let c720 = credits(None, dur); + let c1080 = credits(Some(KinoviHappyHorse1p0OutputResolution::TenEightyP), dur); + assert_eq!(c1080, c720 * 2, "1080p should be 2× 720p at {}s", dur); + } } #[test] fn cost_scales_with_duration() { - let c4 = usd_cents(None, 4, KinoviBatchCountRaw::One); - let c10 = usd_cents(None, 10, KinoviBatchCountRaw::One); - let c15 = usd_cents(None, 15, KinoviBatchCountRaw::One); - assert!(c4 < c10); + let c3 = credits(None, 3); + let c10 = credits(None, 10); + let c15 = credits(None, 15); + assert!(c3 < c10); assert!(c10 < c15); } - #[test] - fn cost_scales_with_batch() { - let b1 = usd_cents(None, 5, KinoviBatchCountRaw::One); - let b2 = usd_cents(None, 5, KinoviBatchCountRaw::Two); - let b4 = usd_cents(None, 5, KinoviBatchCountRaw::Four); - assert!(b1 < b2); - assert!(b2 < b4); - } - #[test] fn not_free() { let est = KinoviHappyHorse1p0CostState { - resolution: None, duration_seconds: 5, batch_count: KinoviBatchCountRaw::One, + resolution: None, duration_seconds: 5, batch_count: None, }.estimate_cost(); assert!(!est.is_free); } } - // ── from_draft() tests ── + // ── from_draft() ── mod from_draft_tests { use super::*; @@ -196,22 +205,19 @@ mod tests { fn from_draft_720p_default() { let draft = make_draft(5, 1, None); let cost = KinoviHappyHorse1p0CostState::from_draft(&draft); - assert!(cost.resolution.is_none()); assert_eq!(cost.duration_seconds, 5); - assert!(matches!(cost.batch_count, KinoviBatchCountRaw::One)); + assert_eq!(cost.estimate_cost().cost_in_credits, Some(165)); } #[test] fn from_draft_1080p_batch_4() { let draft = make_draft(5, 4, Some(CommonResolution::TenEightyP)); let cost = KinoviHappyHorse1p0CostState::from_draft(&draft); - assert!(matches!(cost.resolution, Some(KinoviOutputResolutionRaw::TenEightyP))); - assert!(matches!(cost.batch_count, KinoviBatchCountRaw::Four)); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(933)); + assert_eq!(cost.estimate_cost().cost_in_credits, Some(330 * 4)); } } - // ── from_request() tests ── + // ── from_request() ── mod from_request_tests { use super::*; @@ -220,9 +226,7 @@ mod tests { fn from_request_default() { let req = make_request_state(None, 5, None); let cost = KinoviHappyHorse1p0CostState::from_request(&req); - assert!(cost.resolution.is_none()); - assert_eq!(cost.duration_seconds, 5); - assert!(matches!(cost.batch_count, KinoviBatchCountRaw::One)); + assert_eq!(cost.estimate_cost().cost_in_credits, Some(165)); } #[test] @@ -231,18 +235,18 @@ mod tests { Some(KinoviHappyHorse1p0OutputResolution::TenEightyP), 5, None, ); let cost = KinoviHappyHorse1p0CostState::from_request(&req); - assert!(matches!(cost.resolution, Some(KinoviOutputResolutionRaw::TenEightyP))); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(233)); + assert_eq!(cost.estimate_cost().cost_in_credits, Some(330)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(171)); } } - // ── Cross-check: from_draft matches from_request ── + // ── Cross-check: draft matches request ── mod cross_check { use super::*; #[test] - fn draft_and_request_produce_same_cost_720p() { + fn draft_and_request_produce_same_cost() { let draft = make_draft(5, 1, Some(CommonResolution::SevenTwentyP)); let req = make_request_state( Some(KinoviHappyHorse1p0OutputResolution::SevenTwentyP), 5, None, @@ -254,7 +258,7 @@ mod tests { } #[test] - fn draft_and_request_produce_same_cost_1080p() { + fn draft_and_request_produce_same_cost_1080p_batch_2() { let draft = make_draft(10, 2, Some(CommonResolution::TenEightyP)); let req = make_request_state( Some(KinoviHappyHorse1p0OutputResolution::TenEightyP), 10, @@ -267,38 +271,30 @@ mod tests { } } - // ── Credits spot checks ── - - mod credits { - use super::*; - - #[test] - fn credits_720p() { - let est = KinoviHappyHorse1p0CostState { - resolution: Some(KinoviOutputResolutionRaw::SevenTwentyP), - duration_seconds: 5, - batch_count: KinoviBatchCountRaw::One, - }.estimate_cost(); - assert_eq!(est.cost_in_credits, Some(200)); - } - - #[test] - fn credits_1080p() { - let est = KinoviHappyHorse1p0CostState { - resolution: Some(KinoviOutputResolutionRaw::TenEightyP), - duration_seconds: 5, - batch_count: KinoviBatchCountRaw::One, - }.estimate_cost(); - assert_eq!(est.cost_in_credits, Some(450)); - } - } - // ── Helpers ── - fn usd_cents( - resolution: Option, + fn credits( + resolution: Option, duration_seconds: u8, - batch_count: KinoviBatchCountRaw, + ) -> u64 { + credits_with_batch(resolution, duration_seconds, None) + } + + fn credits_with_batch( + resolution: Option, + duration_seconds: u8, + batch_count: Option, + ) -> u64 { + KinoviHappyHorse1p0CostState { resolution, duration_seconds, batch_count } + .estimate_cost() + .cost_in_credits + .unwrap() + } + + fn usd_cents( + resolution: Option, + duration_seconds: u8, + batch_count: Option, ) -> u64 { KinoviHappyHorse1p0CostState { resolution, duration_seconds, batch_count } .estimate_cost() @@ -335,7 +331,7 @@ mod tests { batch_count: Option, ) -> KinoviHappyHorse1p0RequestState { KinoviHappyHorse1p0RequestState { - request: seedance2pro_client::generate::video::generate_happy_horse_1p0::GenerateHappyHorse1p0Request { + request: GenerateHappyHorse1p0Request { prompt: "test".to_string(), aspect_ratio: None, output_resolution: resolution,