diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs index 305a9053f9..f508b2e3ea 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs @@ -62,6 +62,13 @@ use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::build:: use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::build::build_gmicloud_seedance_2p0_u; use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::build::build_gmicloud_seedance_2p0_u_fast; use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video::build::build_grok_api_grok_imagine_video; +use crate::generate::generate_video_v2::providers::fal::kling_1_6_pro::build::build_fal_kling_1_6_pro; +use crate::generate::generate_video_v2::providers::fal::kling_2_1_master::build::build_fal_kling_2_1_master; +use crate::generate::generate_video_v2::providers::fal::kling_2_1_pro::build::build_fal_kling_2_1_pro; +use crate::generate::generate_video_v2::providers::fal::kling_2_5_turbo_pro::build::build_fal_kling_2_5_turbo_pro; +use crate::generate::generate_video_v2::providers::fal::kling_2_6_pro::build::build_fal_kling_2_6_pro; +use crate::generate::generate_video_v2::providers::fal::kling_3p0_pro::build::build_fal_kling_3p0_pro; +use crate::generate::generate_video_v2::providers::fal::kling_3p0_standard::build::build_fal_kling_3p0_standard; use crate::generate::generate_video_v2::providers::fal::seedance_1p0_lite::build::build_fal_seedance_1p0_lite; use crate::generate::generate_video_v2::providers::fal::seedance_1p5_pro::build::build_fal_seedance_1p5_pro; use crate::generate::generate_video_v2::providers::fal::veo_2::build::build_fal_veo_2; @@ -173,6 +180,13 @@ impl GenerateVideoRequestBuilder { (Provider::Artcraft, CommonVideoModel::PreviewModel) => true, (Provider::Artcraft, CommonVideoModel::PreviewModelFast) => true, // Fal + (Provider::Fal, CommonVideoModel::Kling16Pro) => true, + (Provider::Fal, CommonVideoModel::Kling21Master) => true, + (Provider::Fal, CommonVideoModel::Kling21Pro) => true, + (Provider::Fal, CommonVideoModel::Kling2p5TurboPro) => true, + (Provider::Fal, CommonVideoModel::Kling2p6Pro) => true, + (Provider::Fal, CommonVideoModel::Kling3p0Pro) => true, + (Provider::Fal, CommonVideoModel::Kling3p0Standard) => true, (Provider::Fal, CommonVideoModel::Seedance10Lite) => true, (Provider::Fal, CommonVideoModel::Seedance1p5Pro) => true, (Provider::Fal, CommonVideoModel::Veo2) => true, @@ -208,6 +222,13 @@ impl GenerateVideoRequestBuilder { (Provider::Artcraft, CommonVideoModel::PreviewModel) => build_artcraft_preview_model(self), (Provider::Artcraft, CommonVideoModel::PreviewModelFast) => build_artcraft_preview_model_fast(self), // Fal + (Provider::Fal, CommonVideoModel::Kling16Pro) => build_fal_kling_1_6_pro(self), + (Provider::Fal, CommonVideoModel::Kling21Master) => build_fal_kling_2_1_master(self), + (Provider::Fal, CommonVideoModel::Kling21Pro) => build_fal_kling_2_1_pro(self), + (Provider::Fal, CommonVideoModel::Kling2p5TurboPro) => build_fal_kling_2_5_turbo_pro(self), + (Provider::Fal, CommonVideoModel::Kling2p6Pro) => build_fal_kling_2_6_pro(self), + (Provider::Fal, CommonVideoModel::Kling3p0Pro) => build_fal_kling_3p0_pro(self), + (Provider::Fal, CommonVideoModel::Kling3p0Standard) => build_fal_kling_3p0_standard(self), (Provider::Fal, CommonVideoModel::Seedance10Lite) => build_fal_seedance_1p0_lite(self), (Provider::Fal, CommonVideoModel::Seedance1p5Pro) => build_fal_seedance_1p5_pro(self), (Provider::Fal, CommonVideoModel::Veo2) => build_fal_veo_2(self), diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/cost_parity_tests.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/cost_parity_tests.rs index 732421070f..0940e47e45 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/cost_parity_tests.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/cost_parity_tests.rs @@ -287,6 +287,320 @@ fn v2_cost(builder: GenerateVideoRequestBuilder) -> Option { .and_then(|estimate| estimate.cost_in_usd_cents) } +// ── Kling shared helpers ── + +mod kling_helpers { + use super::*; + + pub fn kling_aspect_ratios() -> &'static [Option] { + &[ + None, + Some(CommonAspectRatio::Square), + Some(CommonAspectRatio::WideSixteenByNine), + Some(CommonAspectRatio::TallNineBySixteen), + Some(CommonAspectRatio::Auto), + // Unsupported — falls back to 16:9 for non-ErrorOut. + Some(CommonAspectRatio::WideFourByThree), + ] + } +} + +// ── Kling 1.6 Pro ── + +mod kling_1_6_pro { + use super::*; + use super::kling_helpers::kling_aspect_ratios; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling16Pro, + provider: Provider::Fal, + start_frame: Some(ImageRef::Url(DUMMY_IMAGE.to_string())), + ..Default::default() + } + } + + #[test] + fn cost_parity_full_combinatorial() { + let durations = [None, Some(5u16), Some(10)]; + let mut combos = 0; + for &aspect_ratio in kling_aspect_ratios() { + for &duration in &durations { + for include_end_frame in [false, true] { + let mut builder = base_builder(); + builder.aspect_ratio = aspect_ratio; + builder.duration_seconds = duration; + if include_end_frame { + builder.end_frame = Some(ImageRef::Url(DUMMY_END_FRAME.to_string())); + } + let v1 = v1_cost(&builder); + let v2 = v2_cost(builder.clone()); + assert_eq!(v1, v2, "kling_1_6_pro: ar={:?} dur={:?} end={} → v1={:?} v2={:?}", + aspect_ratio, duration, include_end_frame, v1, v2); + combos += 1; + } + } + } + assert_eq!(combos, 6 * 3 * 2); + } +} + +// ── Kling 2.1 Pro ── + +mod kling_2_1_pro { + use super::*; + use super::kling_helpers::kling_aspect_ratios; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling21Pro, + provider: Provider::Fal, + start_frame: Some(ImageRef::Url(DUMMY_IMAGE.to_string())), + ..Default::default() + } + } + + #[test] + fn cost_parity_full_combinatorial() { + let durations = [None, Some(5u16), Some(10)]; + let mut combos = 0; + for &aspect_ratio in kling_aspect_ratios() { + for &duration in &durations { + for include_end_frame in [false, true] { + let mut builder = base_builder(); + builder.aspect_ratio = aspect_ratio; + builder.duration_seconds = duration; + if include_end_frame { + builder.end_frame = Some(ImageRef::Url(DUMMY_END_FRAME.to_string())); + } + let v1 = v1_cost(&builder); + let v2 = v2_cost(builder.clone()); + assert_eq!(v1, v2, "kling_2_1_pro: ar={:?} dur={:?} end={} → v1={:?} v2={:?}", + aspect_ratio, duration, include_end_frame, v1, v2); + combos += 1; + } + } + } + assert_eq!(combos, 6 * 3 * 2); + } +} + +// ── Kling 2.1 Master ── + +mod kling_2_1_master { + use super::*; + use super::kling_helpers::kling_aspect_ratios; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling21Master, + provider: Provider::Fal, + start_frame: Some(ImageRef::Url(DUMMY_IMAGE.to_string())), + ..Default::default() + } + } + + #[test] + fn cost_parity_full_combinatorial() { + let durations = [None, Some(5u16), Some(10)]; + let mut combos = 0; + for &aspect_ratio in kling_aspect_ratios() { + for &duration in &durations { + let mut builder = base_builder(); + builder.aspect_ratio = aspect_ratio; + builder.duration_seconds = duration; + let v1 = v1_cost(&builder); + let v2 = v2_cost(builder.clone()); + assert_eq!(v1, v2, "kling_2_1_master: ar={:?} dur={:?} → v1={:?} v2={:?}", + aspect_ratio, duration, v1, v2); + combos += 1; + } + } + assert_eq!(combos, 6 * 3); + } +} + +// ── Kling 2.5 Turbo Pro ── + +mod kling_2_5_turbo_pro { + use super::*; + use super::kling_helpers::kling_aspect_ratios; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling2p5TurboPro, + provider: Provider::Fal, + ..Default::default() + } + } + + #[test] + fn cost_parity_full_combinatorial() { + let durations = [None, Some(5u16), Some(10)]; + let mut combos = 0; + for &aspect_ratio in kling_aspect_ratios() { + for &duration in &durations { + for has_start in [false, true] { + for include_end in [false, true] { + if include_end && !has_start { continue; } + let mut builder = base_builder(); + builder.aspect_ratio = aspect_ratio; + builder.duration_seconds = duration; + if has_start { + builder.start_frame = Some(ImageRef::Url(DUMMY_IMAGE.to_string())); + } + if include_end { + builder.end_frame = Some(ImageRef::Url(DUMMY_END_FRAME.to_string())); + } + let v1 = v1_cost(&builder); + let v2 = v2_cost(builder.clone()); + assert_eq!(v1, v2, "kling_2_5_turbo_pro: ar={:?} dur={:?} start={} end={} → v1={:?} v2={:?}", + aspect_ratio, duration, has_start, include_end, v1, v2); + combos += 1; + } + } + } + } + assert!(combos >= 50, "expected ≥50 combos, got {}", combos); + } +} + +// ── Kling 2.6 Pro ── + +mod kling_2_6_pro { + use super::*; + use super::kling_helpers::kling_aspect_ratios; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling2p6Pro, + provider: Provider::Fal, + ..Default::default() + } + } + + #[test] + fn cost_parity_full_combinatorial() { + let durations = [None, Some(5u16), Some(10)]; + let audios = [None, Some(true), Some(false)]; + let mut combos = 0; + for &aspect_ratio in kling_aspect_ratios() { + for &duration in &durations { + for &generate_audio in &audios { + for has_start in [false, true] { + let mut builder = base_builder(); + builder.aspect_ratio = aspect_ratio; + builder.duration_seconds = duration; + builder.generate_audio = generate_audio; + if has_start { + builder.start_frame = Some(ImageRef::Url(DUMMY_IMAGE.to_string())); + } + let v1 = v1_cost(&builder); + let v2 = v2_cost(builder.clone()); + assert_eq!(v1, v2, "kling_2_6_pro: ar={:?} dur={:?} audio={:?} start={} → v1={:?} v2={:?}", + aspect_ratio, duration, generate_audio, has_start, v1, v2); + combos += 1; + } + } + } + } + assert_eq!(combos, 6 * 3 * 3 * 2); + } +} + +// ── Kling 3.0 Pro ── + +mod kling_3p0_pro { + use super::*; + use super::kling_helpers::kling_aspect_ratios; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling3p0Pro, + provider: Provider::Fal, + ..Default::default() + } + } + + #[test] + fn cost_parity_full_combinatorial() { + // 3-15s inclusive, plus None and out-of-range. + let durations = [None, Some(3u16), Some(5), Some(10), Some(15), Some(2), Some(16)]; + let audios = [None, Some(true), Some(false)]; + let mut combos = 0; + for &aspect_ratio in kling_aspect_ratios() { + for &duration in &durations { + for &generate_audio in &audios { + for has_start in [false, true] { + for include_end in [false, true] { + if include_end && !has_start { continue; } + let mut builder = base_builder(); + builder.aspect_ratio = aspect_ratio; + builder.duration_seconds = duration; + builder.generate_audio = generate_audio; + if has_start { + builder.start_frame = Some(ImageRef::Url(DUMMY_IMAGE.to_string())); + } + if include_end { + builder.end_frame = Some(ImageRef::Url(DUMMY_END_FRAME.to_string())); + } + let v1 = v1_cost(&builder); + let v2 = v2_cost(builder.clone()); + assert_eq!(v1, v2, "kling_3p0_pro: ar={:?} dur={:?} audio={:?} start={} end={} → v1={:?} v2={:?}", + aspect_ratio, duration, generate_audio, has_start, include_end, v1, v2); + combos += 1; + } + } + } + } + } + assert!(combos >= 300, "expected ≥300 combos, got {}", combos); + } +} + +// ── Kling 3.0 Standard ── + +mod kling_3p0_standard { + use super::*; + use super::kling_helpers::kling_aspect_ratios; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling3p0Standard, + provider: Provider::Fal, + ..Default::default() + } + } + + #[test] + fn cost_parity_full_combinatorial() { + let durations = [None, Some(3u16), Some(5), Some(10), Some(15), Some(2), Some(16)]; + let audios = [None, Some(true), Some(false)]; + let mut combos = 0; + for &aspect_ratio in kling_aspect_ratios() { + for &duration in &durations { + for &generate_audio in &audios { + for has_start in [false, true] { + let mut builder = base_builder(); + builder.aspect_ratio = aspect_ratio; + builder.duration_seconds = duration; + builder.generate_audio = generate_audio; + if has_start { + builder.start_frame = Some(ImageRef::Url(DUMMY_IMAGE.to_string())); + } + let v1 = v1_cost(&builder); + let v2 = v2_cost(builder.clone()); + assert_eq!(v1, v2, "kling_3p0_standard: ar={:?} dur={:?} audio={:?} start={} → v1={:?} v2={:?}", + aspect_ratio, duration, generate_audio, has_start, v1, v2); + combos += 1; + } + } + } + } + assert!(combos >= 200, "expected ≥200 combos, got {}", combos); + } +} + // ── Veo 2 ── mod veo_2 { diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_1_6_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_1_6_pro/build.rs new file mode 100644 index 0000000000..d86c40494a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_1_6_pro/build.rs @@ -0,0 +1,166 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_v1p6_pro_image_to_video_webhook::{ + Kling1p6ProAspectRatio, Kling1p6ProDuration, Kling1p6ProRequest, +}; + +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::image_ref::ImageRef; +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_v2::providers::fal::kling_1_6_pro::request::FalKling16ProRequestState; +use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + +pub fn build_fal_kling_1_6_pro( + builder: GenerateVideoRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + let image_url = require_url(builder.start_frame.clone(), "start_frame", "Kling 1.6 Pro requires a starting frame")?; + let end_frame_image_url = optional_url(builder.end_frame.clone())?; + let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio, strategy)?; + let duration = plan_duration(builder.duration_seconds, strategy)?; + + let request = Kling1p6ProRequest { + image_url, + end_frame_image_url, + prompt: builder.prompt.clone().unwrap_or_default(), + duration, + aspect_ratio, + }; + + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling16Pro( + FalKling16ProRequestState { request }, + ))) +} + +pub(crate) fn require_url( + image_ref: Option, + field: &'static str, + reason: &str, +) -> Result { + match image_ref { + Some(ImageRef::Url(url)) => Ok(url), + Some(ImageRef::MediaFileToken(_)) => { + Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls)) + } + None => Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field, + value: reason.to_string(), + })), + } +} + +pub(crate) fn optional_url(image_ref: Option) -> Result, ArtcraftRouterError> { + match image_ref { + None => Ok(None), + Some(ImageRef::Url(url)) => Ok(Some(url)), + Some(ImageRef::MediaFileToken(_)) => { + Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls)) + } + } +} + +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + use Kling1p6ProAspectRatio as Ar; + match aspect_ratio { + None => Ok(Ar::WideSixteenNine), + + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Ar::Square), + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::WideSixteenNine), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::TallNineSixteen), + + Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(Ar::WideSixteenNine), + + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", other), + })) + } + _ => Ok(Ar::WideSixteenNine), + }, + } +} + +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + match duration_seconds { + None => Ok(Kling1p6ProDuration::Default), + Some(5) => Ok(Kling1p6ProDuration::FiveSeconds), + Some(10) => Ok(Kling1p6ProDuration::TenSeconds), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", other), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Kling1p6ProDuration::TenSeconds), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Kling1p6ProDuration::FiveSeconds), + }, + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_video_model::CommonVideoModel; + use crate::api::provider::Provider; + + use super::*; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling16Pro, + provider: Provider::Fal, + prompt: Some("test".to_string()), + start_frame: Some(ImageRef::Url("https://example.com/a.png".to_string())), + ..Default::default() + } + } + + #[test] + fn missing_start_frame_errors() { + let mut b = base_builder(); + b.start_frame = None; + assert!(build_fal_kling_1_6_pro(b).is_err()); + } + + #[test] + fn end_frame_is_optional() { + let mut b = base_builder(); + b.end_frame = Some(ImageRef::Url("https://example.com/end.png".to_string())); + assert!(build_fal_kling_1_6_pro(b).is_ok()); + } + + #[test] + fn full_combinatorial_pass() { + let aspect_ratios = [None, Some(CommonAspectRatio::Square), Some(CommonAspectRatio::WideSixteenByNine), Some(CommonAspectRatio::TallNineBySixteen), Some(CommonAspectRatio::Auto)]; + let durations = [None, Some(5u16), Some(10)]; + let mut combos = 0; + for &aspect_ratio in &aspect_ratios { + for &duration in &durations { + for include_end in [false, true] { + let mut b = base_builder(); + b.aspect_ratio = aspect_ratio; + b.duration_seconds = duration; + if include_end { + b.end_frame = Some(ImageRef::Url("https://example.com/end.png".to_string())); + } + assert!(build_fal_kling_1_6_pro(b).is_ok()); + combos += 1; + } + } + } + assert_eq!(combos, 5 * 3 * 2); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_1_6_pro/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_1_6_pro/cost.rs new file mode 100644 index 0000000000..630e7e3216 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_1_6_pro/cost.rs @@ -0,0 +1,61 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_v1p6_pro_image_to_video_webhook::Kling1p6ProDuration; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video_v2::providers::fal::kling_1_6_pro::request::FalKling16ProRequestState; + +#[derive(Clone, Debug)] +pub struct FalKling16ProCostState { + pub is_ten_seconds: bool, +} + +impl FalKling16ProCostState { + pub fn from_request(request: &FalKling16ProRequestState) -> Self { + Self { + is_ten_seconds: matches!(request.request.duration, Kling1p6ProDuration::TenSeconds), + } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + // Mirrors fal_client kling_v1p6_pro: 5s = 48¢, 10s = 95¢ (Default → 5s). + let cost_in_usd_cents: u64 = if self.is_ten_seconds { 95 } else { 48 }; + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + 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::common_video_model::CommonVideoModel; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + + fn cost_cents(duration_seconds: Option) -> u64 { + let b = GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling16Pro, + provider: Provider::Fal, + prompt: Some("test".to_string()), + start_frame: Some(ImageRef::Url("https://example.com/a.png".to_string())), + duration_seconds, + ..Default::default() + }; + b.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap() + } + + #[test] + fn five_seconds_is_48() { assert_eq!(cost_cents(Some(5)), 48); } + + #[test] + fn ten_seconds_is_95() { assert_eq!(cost_cents(Some(10)), 95); } + + #[test] + fn default_duration_is_5s_priced_at_48() { assert_eq!(cost_cents(None), 48); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_1_6_pro/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_1_6_pro/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_1_6_pro/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_1_6_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_1_6_pro/request.rs new file mode 100644 index 0000000000..d1d02df762 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_1_6_pro/request.rs @@ -0,0 +1,40 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use fal_client::requests::webhook::video::image::enqueue_kling_v1p6_pro_image_to_video_webhook::{ + enqueue_kling_v1p6_pro_image_to_video_webhook, Kling1p6ProArgs, Kling1p6ProRequest, +}; + +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; + +#[derive(Clone, Debug)] +pub struct FalKling16ProRequestState { + pub request: Kling1p6ProRequest, +} + +impl FalKling16ProRequestState { + pub async fn send(&self, client: &RouterFalClient) -> Result { + let outbound_request: Arc = Arc::new(self.request.clone()); + + let args = Kling1p6ProArgs { + request: self.request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + + let webhook_response = enqueue_kling_v1p6_pro_image_to_video_webhook(args) + .await + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + maybe_outbound_request: Some(outbound_request), + })) + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_master/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_master/build.rs new file mode 100644 index 0000000000..054de622ec --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_master/build.rs @@ -0,0 +1,123 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_v2p1_master_image_to_video_webhook::{ + Kling2p1MasterAspectRatio, Kling2p1MasterDuration, Kling2p1MasterRequest, +}; + +use crate::api::common_aspect_ratio::CommonAspectRatio; +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_v2::providers::fal::kling_1_6_pro::build::require_url; +use crate::generate::generate_video_v2::providers::fal::kling_2_1_master::request::FalKling21MasterRequestState; +use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + +pub fn build_fal_kling_2_1_master( + builder: GenerateVideoRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + let image_url = require_url(builder.start_frame.clone(), "start_frame", "Kling 2.1 Master requires a starting frame")?; + if builder.end_frame.is_some() { + return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "end_frame", + value: "Kling 2.1 Master does not support an ending frame".to_string(), + })); + } + + let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio, strategy)?; + let duration = plan_duration(builder.duration_seconds, strategy)?; + + let request = Kling2p1MasterRequest { + image_url, + prompt: builder.prompt.clone().unwrap_or_default(), + duration, + aspect_ratio, + }; + + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling21Master( + FalKling21MasterRequestState { request }, + ))) +} + +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + use Kling2p1MasterAspectRatio as Ar; + match aspect_ratio { + None => Ok(Ar::WideSixteenNine), + + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Ar::Square), + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::WideSixteenNine), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::TallNineSixteen), + + Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(Ar::WideSixteenNine), + + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", other), + })) + } + _ => Ok(Ar::WideSixteenNine), + }, + } +} + +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + match duration_seconds { + None => Ok(Kling2p1MasterDuration::Default), + Some(5) => Ok(Kling2p1MasterDuration::FiveSeconds), + Some(10) => Ok(Kling2p1MasterDuration::TenSeconds), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", other), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Kling2p1MasterDuration::TenSeconds), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Kling2p1MasterDuration::FiveSeconds), + }, + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_video_model::CommonVideoModel; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + + use super::*; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling21Master, + provider: Provider::Fal, + prompt: Some("test".to_string()), + start_frame: Some(ImageRef::Url("https://example.com/a.png".to_string())), + ..Default::default() + } + } + + #[test] + fn missing_start_frame_errors() { + let mut b = base_builder(); + b.start_frame = None; + assert!(build_fal_kling_2_1_master(b).is_err()); + } + + #[test] + fn end_frame_errors() { + let mut b = base_builder(); + b.end_frame = Some(ImageRef::Url("https://example.com/end.png".to_string())); + assert!(build_fal_kling_2_1_master(b).is_err()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_master/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_master/cost.rs new file mode 100644 index 0000000000..f13a25258f --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_master/cost.rs @@ -0,0 +1,61 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_v2p1_master_image_to_video_webhook::Kling2p1MasterDuration; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video_v2::providers::fal::kling_2_1_master::request::FalKling21MasterRequestState; + +#[derive(Clone, Debug)] +pub struct FalKling21MasterCostState { + pub is_ten_seconds: bool, +} + +impl FalKling21MasterCostState { + pub fn from_request(request: &FalKling21MasterRequestState) -> Self { + Self { + is_ten_seconds: matches!(request.request.duration, Kling2p1MasterDuration::TenSeconds), + } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + // Mirrors fal_client kling_v2p1_master: 5s = $1.40, 10s = $2.80. + let cost_in_usd_cents: u64 = if self.is_ten_seconds { 280 } else { 140 }; + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + 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::common_video_model::CommonVideoModel; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + + fn cost_cents(duration_seconds: Option) -> u64 { + let b = GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling21Master, + provider: Provider::Fal, + prompt: Some("test".to_string()), + start_frame: Some(ImageRef::Url("https://example.com/a.png".to_string())), + duration_seconds, + ..Default::default() + }; + b.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap() + } + + #[test] + fn five_seconds_is_140() { assert_eq!(cost_cents(Some(5)), 140); } + + #[test] + fn ten_seconds_is_280() { assert_eq!(cost_cents(Some(10)), 280); } + + #[test] + fn default_duration_is_5s_priced_at_140() { assert_eq!(cost_cents(None), 140); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_master/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_master/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_master/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_master/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_master/request.rs new file mode 100644 index 0000000000..c1be87bfda --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_master/request.rs @@ -0,0 +1,40 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use fal_client::requests::webhook::video::image::enqueue_kling_v2p1_master_image_to_video_webhook::{ + enqueue_kling_v2p1_master_image_to_video_webhook, Kling2p1MasterArgs, Kling2p1MasterRequest, +}; + +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; + +#[derive(Clone, Debug)] +pub struct FalKling21MasterRequestState { + pub request: Kling2p1MasterRequest, +} + +impl FalKling21MasterRequestState { + pub async fn send(&self, client: &RouterFalClient) -> Result { + let outbound_request: Arc = Arc::new(self.request.clone()); + + let args = Kling2p1MasterArgs { + request: self.request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + + let webhook_response = enqueue_kling_v2p1_master_image_to_video_webhook(args) + .await + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + maybe_outbound_request: Some(outbound_request), + })) + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_pro/build.rs new file mode 100644 index 0000000000..628b51bbf0 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_pro/build.rs @@ -0,0 +1,85 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_v2p1_pro_image_to_video_webhook::{ + Kling2p1ProAspectRatio, Kling2p1ProDuration, Kling2p1ProRequest, +}; + +use crate::api::common_aspect_ratio::CommonAspectRatio; +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_v2::providers::fal::kling_1_6_pro::build::{optional_url, require_url}; +use crate::generate::generate_video_v2::providers::fal::kling_2_1_pro::request::FalKling21ProRequestState; +use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + +pub fn build_fal_kling_2_1_pro( + builder: GenerateVideoRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + let image_url = require_url(builder.start_frame.clone(), "start_frame", "Kling 2.1 Pro requires a starting frame")?; + let end_frame_image_url = optional_url(builder.end_frame.clone())?; + let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio, strategy)?; + let duration = plan_duration(builder.duration_seconds, strategy)?; + + let request = Kling2p1ProRequest { + image_url, + end_frame_image_url, + prompt: builder.prompt.clone().unwrap_or_default(), + duration, + aspect_ratio, + }; + + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling21Pro( + FalKling21ProRequestState { request }, + ))) +} + +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + use Kling2p1ProAspectRatio as Ar; + match aspect_ratio { + None => Ok(Ar::WideSixteenNine), + + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Ar::Square), + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::WideSixteenNine), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::TallNineSixteen), + + Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(Ar::WideSixteenNine), + + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", other), + })) + } + _ => Ok(Ar::WideSixteenNine), + }, + } +} + +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + match duration_seconds { + None => Ok(Kling2p1ProDuration::Default), + Some(5) => Ok(Kling2p1ProDuration::FiveSeconds), + Some(10) => Ok(Kling2p1ProDuration::TenSeconds), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", other), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Kling2p1ProDuration::TenSeconds), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Kling2p1ProDuration::FiveSeconds), + }, + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_pro/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_pro/cost.rs new file mode 100644 index 0000000000..79ae780d46 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_pro/cost.rs @@ -0,0 +1,61 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_v2p1_pro_image_to_video_webhook::Kling2p1ProDuration; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video_v2::providers::fal::kling_2_1_pro::request::FalKling21ProRequestState; + +#[derive(Clone, Debug)] +pub struct FalKling21ProCostState { + pub is_ten_seconds: bool, +} + +impl FalKling21ProCostState { + pub fn from_request(request: &FalKling21ProRequestState) -> Self { + Self { + is_ten_seconds: matches!(request.request.duration, Kling2p1ProDuration::TenSeconds), + } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + // Mirrors fal_client kling_v2p1_pro: 5s = 45¢, 10s = 90¢. + let cost_in_usd_cents: u64 = if self.is_ten_seconds { 90 } else { 45 }; + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + 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::common_video_model::CommonVideoModel; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + + fn cost_cents(duration_seconds: Option) -> u64 { + let b = GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling21Pro, + provider: Provider::Fal, + prompt: Some("test".to_string()), + start_frame: Some(ImageRef::Url("https://example.com/a.png".to_string())), + duration_seconds, + ..Default::default() + }; + b.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap() + } + + #[test] + fn five_seconds_is_45() { assert_eq!(cost_cents(Some(5)), 45); } + + #[test] + fn ten_seconds_is_90() { assert_eq!(cost_cents(Some(10)), 90); } + + #[test] + fn default_duration_is_5s_priced_at_45() { assert_eq!(cost_cents(None), 45); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_pro/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_pro/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_pro/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_pro/request.rs new file mode 100644 index 0000000000..4f5b5fd6cc --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_1_pro/request.rs @@ -0,0 +1,40 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use fal_client::requests::webhook::video::image::enqueue_kling_v2p1_pro_image_to_video_webhook::{ + enqueue_kling_v2p1_pro_image_to_video_webhook, Kling2p1ProArgs, Kling2p1ProRequest, +}; + +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; + +#[derive(Clone, Debug)] +pub struct FalKling21ProRequestState { + pub request: Kling2p1ProRequest, +} + +impl FalKling21ProRequestState { + pub async fn send(&self, client: &RouterFalClient) -> Result { + let outbound_request: Arc = Arc::new(self.request.clone()); + + let args = Kling2p1ProArgs { + request: self.request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + + let webhook_response = enqueue_kling_v2p1_pro_image_to_video_webhook(args) + .await + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + maybe_outbound_request: Some(outbound_request), + })) + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_5_turbo_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_5_turbo_pro/build.rs new file mode 100644 index 0000000000..aea28873e7 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_5_turbo_pro/build.rs @@ -0,0 +1,189 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_v2p5_turbo_pro_image_to_video_webhook::{ + EnqueueKlingV2p5TurboProImageToVideoDurationSeconds, EnqueueKlingV2p5TurboProImageToVideoRequest, +}; +use fal_client::requests::webhook::video::text::enqueue_kling_v2p5_turbo_pro_text_to_video_webhook::{ + EnqueueKlingV2p5TurboProTextToVideoAspectRatio, EnqueueKlingV2p5TurboProTextToVideoDurationSeconds, + EnqueueKlingV2p5TurboProTextToVideoRequest, +}; + +use crate::api::common_aspect_ratio::CommonAspectRatio; +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_v2::providers::fal::kling_1_6_pro::build::optional_url; +use crate::generate::generate_video_v2::providers::fal::kling_2_5_turbo_pro::request::{ + FalKling2p5TurboProMode, FalKling2p5TurboProRequestState, +}; +use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + +#[derive(Debug, Clone, Copy)] +pub(crate) enum PlanAspectRatio { + Square, + SixteenByNine, + NineBySixteen, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum PlanDuration { + Five, + Ten, +} + +pub fn build_fal_kling_2_5_turbo_pro( + builder: GenerateVideoRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio, strategy)?; + let duration = plan_duration(builder.duration_seconds, strategy)?; + let prompt = builder.prompt.clone().unwrap_or_default(); + let negative_prompt = builder.negative_prompt.clone(); + + let mode = match optional_url(builder.start_frame.clone())? { + None => { + if builder.end_frame.is_some() { + return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "end_frame", + value: "Kling 2.5 Turbo Pro requires a start_frame when end_frame is provided".to_string(), + })); + } + FalKling2p5TurboProMode::TextToVideo(EnqueueKlingV2p5TurboProTextToVideoRequest { + prompt, + negative_prompt, + duration: duration.map(to_t2v_duration), + aspect_ratio: aspect_ratio.map(to_t2v_aspect_ratio), + }) + } + Some(image_url) => FalKling2p5TurboProMode::ImageToVideo(EnqueueKlingV2p5TurboProImageToVideoRequest { + prompt, + image_url, + tail_image_url: optional_url(builder.end_frame.clone())?, + negative_prompt, + duration: duration.map(to_i2v_duration), + }), + }; + + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling2p5TurboPro( + FalKling2p5TurboProRequestState { mode }, + ))) +} + +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use PlanAspectRatio as Ar; + match aspect_ratio { + None => Ok(None), + + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(Ar::Square)), + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Ar::SixteenByNine)), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Ar::NineBySixteen)), + + Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(Some(Ar::SixteenByNine)), + + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", other), + })) + } + _ => Ok(Some(Ar::SixteenByNine)), + }, + } +} + +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match duration_seconds { + None => Ok(None), + Some(5) => Ok(Some(PlanDuration::Five)), + Some(10) => Ok(Some(PlanDuration::Ten)), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", other), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(PlanDuration::Ten)), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(PlanDuration::Five)), + }, + } +} + +fn to_t2v_duration(d: PlanDuration) -> EnqueueKlingV2p5TurboProTextToVideoDurationSeconds { + match d { + PlanDuration::Five => EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Five, + PlanDuration::Ten => EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Ten, + } +} + +fn to_t2v_aspect_ratio(a: PlanAspectRatio) -> EnqueueKlingV2p5TurboProTextToVideoAspectRatio { + match a { + PlanAspectRatio::Square => EnqueueKlingV2p5TurboProTextToVideoAspectRatio::Square, + PlanAspectRatio::SixteenByNine => EnqueueKlingV2p5TurboProTextToVideoAspectRatio::SixteenByNine, + PlanAspectRatio::NineBySixteen => EnqueueKlingV2p5TurboProTextToVideoAspectRatio::NineBySixteen, + } +} + +fn to_i2v_duration(d: PlanDuration) -> EnqueueKlingV2p5TurboProImageToVideoDurationSeconds { + match d { + PlanDuration::Five => EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Five, + PlanDuration::Ten => EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Ten, + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_video_model::CommonVideoModel; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + + use super::*; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling2p5TurboPro, + provider: Provider::Fal, + prompt: Some("test".to_string()), + ..Default::default() + } + } + + #[test] + fn no_start_frame_picks_t2v() { + let result = build_fal_kling_2_5_turbo_pro(base_builder()).expect("build"); + if let VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling2p5TurboPro(s)) = result { + assert!(matches!(s.mode, FalKling2p5TurboProMode::TextToVideo(_))); + } else { + panic!("expected FalKling2p5TurboPro"); + } + } + + #[test] + fn start_frame_picks_i2v() { + let mut b = base_builder(); + b.start_frame = Some(ImageRef::Url("https://example.com/a.png".to_string())); + let result = build_fal_kling_2_5_turbo_pro(b).expect("build"); + if let VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling2p5TurboPro(s)) = result { + assert!(matches!(s.mode, FalKling2p5TurboProMode::ImageToVideo(_))); + } else { + panic!("expected FalKling2p5TurboPro"); + } + } + + #[test] + fn end_frame_without_start_frame_errors() { + let mut b = base_builder(); + b.end_frame = Some(ImageRef::Url("https://example.com/end.png".to_string())); + assert!(build_fal_kling_2_5_turbo_pro(b).is_err()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_5_turbo_pro/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_5_turbo_pro/cost.rs new file mode 100644 index 0000000000..c4a70ffcdf --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_5_turbo_pro/cost.rs @@ -0,0 +1,75 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_v2p5_turbo_pro_image_to_video_webhook::EnqueueKlingV2p5TurboProImageToVideoDurationSeconds; +use fal_client::requests::webhook::video::text::enqueue_kling_v2p5_turbo_pro_text_to_video_webhook::EnqueueKlingV2p5TurboProTextToVideoDurationSeconds; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video_v2::providers::fal::kling_2_5_turbo_pro::request::{ + FalKling2p5TurboProMode, FalKling2p5TurboProRequestState, +}; + +#[derive(Clone, Debug)] +pub struct FalKling2p5TurboProCostState { + pub is_ten_seconds: bool, +} + +impl FalKling2p5TurboProCostState { + pub fn from_request(request: &FalKling2p5TurboProRequestState) -> Self { + let is_ten_seconds = match &request.mode { + FalKling2p5TurboProMode::TextToVideo(req) => { + matches!(req.duration, Some(EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Ten)) + } + FalKling2p5TurboProMode::ImageToVideo(req) => { + matches!(req.duration, Some(EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Ten)) + } + }; + Self { is_ten_seconds } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + // Mirrors fal_client kling_v2p5_turbo_pro: 5s = 35¢, 10s = 70¢. None → 5s. + let cost_in_usd_cents: u64 = if self.is_ten_seconds { 70 } else { 35 }; + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + 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::common_video_model::CommonVideoModel; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + + fn cost_cents(duration_seconds: Option, has_start: bool) -> u64 { + let mut b = GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling2p5TurboPro, + provider: Provider::Fal, + prompt: Some("test".to_string()), + duration_seconds, + ..Default::default() + }; + if has_start { + b.start_frame = Some(ImageRef::Url("https://example.com/a.png".to_string())); + } + b.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap() + } + + #[test] + fn t2v_5s_is_35() { assert_eq!(cost_cents(Some(5), false), 35); } + + #[test] + fn t2v_10s_is_70() { assert_eq!(cost_cents(Some(10), false), 70); } + + #[test] + fn i2v_5s_is_35() { assert_eq!(cost_cents(Some(5), true), 35); } + + #[test] + fn default_is_5s_priced_at_35() { assert_eq!(cost_cents(None, false), 35); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_5_turbo_pro/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_5_turbo_pro/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_5_turbo_pro/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_5_turbo_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_5_turbo_pro/request.rs new file mode 100644 index 0000000000..1f33da24fc --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_5_turbo_pro/request.rs @@ -0,0 +1,63 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use fal_client::requests::webhook::video::image::enqueue_kling_v2p5_turbo_pro_image_to_video_webhook::{ + enqueue_kling_v2p5_turbo_pro_image_to_video_webhook, EnqueueKlingV2p5TurboProImageToVideoArgs, + EnqueueKlingV2p5TurboProImageToVideoRequest, +}; +use fal_client::requests::webhook::video::text::enqueue_kling_v2p5_turbo_pro_text_to_video_webhook::{ + enqueue_kling_v2p5_turbo_pro_text_to_video_webhook, EnqueueKlingV2p5TurboProTextToVideoArgs, + EnqueueKlingV2p5TurboProTextToVideoRequest, +}; + +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; + +#[derive(Clone, Debug)] +pub enum FalKling2p5TurboProMode { + TextToVideo(EnqueueKlingV2p5TurboProTextToVideoRequest), + ImageToVideo(EnqueueKlingV2p5TurboProImageToVideoRequest), +} + +#[derive(Clone, Debug)] +pub struct FalKling2p5TurboProRequestState { + pub mode: FalKling2p5TurboProMode, +} + +impl FalKling2p5TurboProRequestState { + pub async fn send(&self, client: &RouterFalClient) -> Result { + let (webhook_response, outbound_request): (_, Arc) = match &self.mode { + FalKling2p5TurboProMode::TextToVideo(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueKlingV2p5TurboProTextToVideoArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_kling_v2p5_turbo_pro_text_to_video_webhook(args).await, outbound) + } + FalKling2p5TurboProMode::ImageToVideo(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueKlingV2p5TurboProImageToVideoArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_kling_v2p5_turbo_pro_image_to_video_webhook(args).await, outbound) + } + }; + + let webhook_response = webhook_response + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + maybe_outbound_request: Some(outbound_request), + })) + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_6_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_6_pro/build.rs new file mode 100644 index 0000000000..bbeb56c800 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_6_pro/build.rs @@ -0,0 +1,186 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_v2p6_pro_image_to_video_webhook::{ + EnqueueKlingV2p6ProImageToVideoDurationSeconds, EnqueueKlingV2p6ProImageToVideoRequest, +}; +use fal_client::requests::webhook::video::text::enqueue_kling_v2p6_pro_text_to_video_webhook::{ + EnqueueKlingV2p6ProTextToVideoAspectRatio, EnqueueKlingV2p6ProTextToVideoDurationSeconds, + EnqueueKlingV2p6ProTextToVideoRequest, +}; + +use crate::api::common_aspect_ratio::CommonAspectRatio; +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_v2::providers::fal::kling_1_6_pro::build::optional_url; +use crate::generate::generate_video_v2::providers::fal::kling_2_6_pro::request::{ + FalKling2p6ProMode, FalKling2p6ProRequestState, +}; +use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + +#[derive(Debug, Clone, Copy)] +pub(crate) enum PlanAspectRatio { + Square, + SixteenByNine, + NineBySixteen, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum PlanDuration { + Five, + Ten, +} + +pub fn build_fal_kling_2_6_pro( + builder: GenerateVideoRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + if builder.end_frame.is_some() { + return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "end_frame", + value: "Kling 2.6 Pro does not support an ending frame".to_string(), + })); + } + + let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio, strategy)?; + let duration = plan_duration(builder.duration_seconds, strategy)?; + let prompt = builder.prompt.clone().unwrap_or_default(); + let negative_prompt = builder.negative_prompt.clone(); + let generate_audio = builder.generate_audio; + + let mode = match optional_url(builder.start_frame.clone())? { + None => FalKling2p6ProMode::TextToVideo(EnqueueKlingV2p6ProTextToVideoRequest { + prompt, + generate_audio, + negative_prompt, + duration: duration.map(to_t2v_duration), + aspect_ratio: aspect_ratio.map(to_t2v_aspect_ratio), + }), + Some(image_url) => FalKling2p6ProMode::ImageToVideo(EnqueueKlingV2p6ProImageToVideoRequest { + prompt, + image_url, + generate_audio, + negative_prompt, + duration: duration.map(to_i2v_duration), + }), + }; + + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling2p6Pro( + FalKling2p6ProRequestState { mode }, + ))) +} + +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use PlanAspectRatio as Ar; + match aspect_ratio { + None => Ok(None), + + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(Ar::Square)), + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Ar::SixteenByNine)), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Ar::NineBySixteen)), + + Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(Some(Ar::SixteenByNine)), + + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", other), + })) + } + _ => Ok(Some(Ar::SixteenByNine)), + }, + } +} + +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match duration_seconds { + None => Ok(None), + Some(5) => Ok(Some(PlanDuration::Five)), + Some(10) => Ok(Some(PlanDuration::Ten)), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", other), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(PlanDuration::Ten)), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(PlanDuration::Five)), + }, + } +} + +fn to_t2v_duration(d: PlanDuration) -> EnqueueKlingV2p6ProTextToVideoDurationSeconds { + match d { + PlanDuration::Five => EnqueueKlingV2p6ProTextToVideoDurationSeconds::Five, + PlanDuration::Ten => EnqueueKlingV2p6ProTextToVideoDurationSeconds::Ten, + } +} + +fn to_t2v_aspect_ratio(a: PlanAspectRatio) -> EnqueueKlingV2p6ProTextToVideoAspectRatio { + match a { + PlanAspectRatio::Square => EnqueueKlingV2p6ProTextToVideoAspectRatio::Square, + PlanAspectRatio::SixteenByNine => EnqueueKlingV2p6ProTextToVideoAspectRatio::SixteenByNine, + PlanAspectRatio::NineBySixteen => EnqueueKlingV2p6ProTextToVideoAspectRatio::NineBySixteen, + } +} + +fn to_i2v_duration(d: PlanDuration) -> EnqueueKlingV2p6ProImageToVideoDurationSeconds { + match d { + PlanDuration::Five => EnqueueKlingV2p6ProImageToVideoDurationSeconds::Five, + PlanDuration::Ten => EnqueueKlingV2p6ProImageToVideoDurationSeconds::Ten, + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_video_model::CommonVideoModel; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + + use super::*; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling2p6Pro, + provider: Provider::Fal, + prompt: Some("test".to_string()), + ..Default::default() + } + } + + #[test] + fn end_frame_errors() { + let mut b = base_builder(); + b.end_frame = Some(ImageRef::Url("https://example.com/end.png".to_string())); + assert!(build_fal_kling_2_6_pro(b).is_err()); + } + + #[test] + fn no_start_frame_picks_t2v() { + let result = build_fal_kling_2_6_pro(base_builder()).expect("build"); + if let VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling2p6Pro(s)) = result { + assert!(matches!(s.mode, FalKling2p6ProMode::TextToVideo(_))); + } else { panic!("expected FalKling2p6Pro"); } + } + + #[test] + fn start_frame_picks_i2v() { + let mut b = base_builder(); + b.start_frame = Some(ImageRef::Url("https://example.com/a.png".to_string())); + let result = build_fal_kling_2_6_pro(b).expect("build"); + if let VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling2p6Pro(s)) = result { + assert!(matches!(s.mode, FalKling2p6ProMode::ImageToVideo(_))); + } else { panic!("expected FalKling2p6Pro"); } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_6_pro/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_6_pro/cost.rs new file mode 100644 index 0000000000..1b7cc46354 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_6_pro/cost.rs @@ -0,0 +1,88 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_v2p6_pro_image_to_video_webhook::EnqueueKlingV2p6ProImageToVideoDurationSeconds; +use fal_client::requests::webhook::video::text::enqueue_kling_v2p6_pro_text_to_video_webhook::EnqueueKlingV2p6ProTextToVideoDurationSeconds; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video_v2::providers::fal::kling_2_6_pro::request::{ + FalKling2p6ProMode, FalKling2p6ProRequestState, +}; + +#[derive(Clone, Debug)] +pub struct FalKling2p6ProCostState { + pub is_ten_seconds: bool, + pub generate_audio: bool, +} + +impl FalKling2p6ProCostState { + pub fn from_request(request: &FalKling2p6ProRequestState) -> Self { + let (is_ten_seconds, generate_audio) = match &request.mode { + FalKling2p6ProMode::TextToVideo(req) => ( + matches!(req.duration, Some(EnqueueKlingV2p6ProTextToVideoDurationSeconds::Ten)), + req.generate_audio.unwrap_or(true), + ), + FalKling2p6ProMode::ImageToVideo(req) => ( + matches!(req.duration, Some(EnqueueKlingV2p6ProImageToVideoDurationSeconds::Ten)), + req.generate_audio.unwrap_or(true), + ), + }; + Self { is_ten_seconds, generate_audio } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + // Mirrors fal_client kling_v2p6_pro: + // audio off: $0.07/sec → 5s=35¢, 10s=70¢ + // audio on: $0.14/sec → 5s=70¢, 10s=140¢ + let cost_in_usd_cents: u64 = match (self.generate_audio, self.is_ten_seconds) { + (false, false) => 35, + (false, true) => 70, + (true, false) => 70, + (true, true) => 140, + }; + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + 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::common_video_model::CommonVideoModel; + use crate::api::provider::Provider; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + + fn cost_cents(duration_seconds: Option, generate_audio: Option) -> u64 { + let b = GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling2p6Pro, + provider: Provider::Fal, + prompt: Some("test".to_string()), + duration_seconds, + generate_audio, + ..Default::default() + }; + b.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap() + } + + #[test] + fn audio_off_5s_is_35() { assert_eq!(cost_cents(Some(5), Some(false)), 35); } + + #[test] + fn audio_off_10s_is_70() { assert_eq!(cost_cents(Some(10), Some(false)), 70); } + + #[test] + fn audio_on_5s_is_70() { assert_eq!(cost_cents(Some(5), Some(true)), 70); } + + #[test] + fn audio_on_10s_is_140() { assert_eq!(cost_cents(Some(10), Some(true)), 140); } + + #[test] + fn audio_default_is_on() { + // None → audio defaults to true. + assert_eq!(cost_cents(Some(5), None), cost_cents(Some(5), Some(true))); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_6_pro/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_6_pro/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_6_pro/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_6_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_6_pro/request.rs new file mode 100644 index 0000000000..b5638b34e4 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_2_6_pro/request.rs @@ -0,0 +1,63 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use fal_client::requests::webhook::video::image::enqueue_kling_v2p6_pro_image_to_video_webhook::{ + enqueue_kling_v2p6_pro_image_to_video_webhook, EnqueueKlingV2p6ProImageToVideoArgs, + EnqueueKlingV2p6ProImageToVideoRequest, +}; +use fal_client::requests::webhook::video::text::enqueue_kling_v2p6_pro_text_to_video_webhook::{ + enqueue_kling_v2p6_pro_text_to_video_webhook, EnqueueKlingV2p6ProTextToVideoArgs, + EnqueueKlingV2p6ProTextToVideoRequest, +}; + +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; + +#[derive(Clone, Debug)] +pub enum FalKling2p6ProMode { + TextToVideo(EnqueueKlingV2p6ProTextToVideoRequest), + ImageToVideo(EnqueueKlingV2p6ProImageToVideoRequest), +} + +#[derive(Clone, Debug)] +pub struct FalKling2p6ProRequestState { + pub mode: FalKling2p6ProMode, +} + +impl FalKling2p6ProRequestState { + pub async fn send(&self, client: &RouterFalClient) -> Result { + let (webhook_response, outbound_request): (_, Arc) = match &self.mode { + FalKling2p6ProMode::TextToVideo(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueKlingV2p6ProTextToVideoArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_kling_v2p6_pro_text_to_video_webhook(args).await, outbound) + } + FalKling2p6ProMode::ImageToVideo(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueKlingV2p6ProImageToVideoArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_kling_v2p6_pro_image_to_video_webhook(args).await, outbound) + } + }; + + let webhook_response = webhook_response + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + maybe_outbound_request: Some(outbound_request), + })) + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_pro/build.rs new file mode 100644 index 0000000000..466f05364a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_pro/build.rs @@ -0,0 +1,253 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_3p0_pro_image_to_video_webhook::{ + EnqueueKling3p0ProImageToVideoAspectRatio, EnqueueKling3p0ProImageToVideoDuration, + EnqueueKling3p0ProImageToVideoRequest, +}; +use fal_client::requests::webhook::video::text::enqueue_kling_3p0_pro_text_to_video_webhook::{ + EnqueueKling3p0ProTextToVideoAspectRatio, EnqueueKling3p0ProTextToVideoDuration, + EnqueueKling3p0ProTextToVideoRequest, +}; + +use crate::api::common_aspect_ratio::CommonAspectRatio; +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_v2::providers::fal::kling_1_6_pro::build::optional_url; +use crate::generate::generate_video_v2::providers::fal::kling_3p0_pro::request::{ + FalKling3p0ProMode, FalKling3p0ProRequestState, +}; +use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + +#[derive(Debug, Clone, Copy)] +pub(crate) enum PlanAspectRatio { + Square, + SixteenByNine, + NineBySixteen, +} + +/// 3-15 seconds inclusive — represented as a `u8` because the fal_client side +/// has 13 enum variants (Three..Fifteen). Stored here so the cost calculator +/// can recover the exact second count for billing. +#[derive(Debug, Clone, Copy)] +pub(crate) struct PlanDuration(pub u8); + +const MIN_DURATION: u16 = 3; +const MAX_DURATION: u16 = 15; + +pub fn build_fal_kling_3p0_pro( + builder: GenerateVideoRequestBuilder, +) -> Result { + let state = build_kling_3p0_pro_state(builder)?; + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling3p0Pro(state))) +} + +fn build_kling_3p0_pro_state( + builder: GenerateVideoRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio, strategy)?; + let duration = plan_duration(builder.duration_seconds, strategy)?; + let prompt = builder.prompt.clone().unwrap_or_default(); + let negative_prompt = builder.negative_prompt.clone(); + let generate_audio = builder.generate_audio; + + let mode = match optional_url(builder.start_frame.clone())? { + None => { + if builder.end_frame.is_some() { + return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "end_frame", + value: "Kling 3.0 Pro requires a start_frame when end_frame is provided".to_string(), + })); + } + FalKling3p0ProMode::TextToVideo(EnqueueKling3p0ProTextToVideoRequest { + prompt, + generate_audio, + negative_prompt, + duration: duration.map(to_t2v_duration), + aspect_ratio: aspect_ratio.map(to_t2v_aspect_ratio), + shot_type: None, + }) + } + Some(image_url) => FalKling3p0ProMode::ImageToVideo(EnqueueKling3p0ProImageToVideoRequest { + prompt, + image_url, + end_image_url: optional_url(builder.end_frame.clone())?, + generate_audio, + negative_prompt, + duration: duration.map(to_i2v_duration), + aspect_ratio: aspect_ratio.map(to_i2v_aspect_ratio), + shot_type: None, + }), + }; + + Ok(FalKling3p0ProRequestState { mode }) +} + +pub(crate) fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use PlanAspectRatio as Ar; + match aspect_ratio { + None => Ok(None), + + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(Ar::Square)), + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Ar::SixteenByNine)), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Ar::NineBySixteen)), + + Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(Some(Ar::SixteenByNine)), + + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", other), + })) + } + _ => Ok(Some(Ar::SixteenByNine)), + }, + } +} + +pub(crate) fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match duration_seconds { + None => Ok(None), + Some(d) if (MIN_DURATION..=MAX_DURATION).contains(&d) => Ok(Some(PlanDuration(d as u8))), + Some(d) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", d), + })) + } + _ => Ok(Some(PlanDuration(d.clamp(MIN_DURATION, MAX_DURATION) as u8))), + }, + } +} + +fn to_t2v_duration(d: PlanDuration) -> EnqueueKling3p0ProTextToVideoDuration { + use EnqueueKling3p0ProTextToVideoDuration as D; + match d.0 { + 3 => D::ThreeSeconds, + 4 => D::FourSeconds, + 5 => D::FiveSeconds, + 6 => D::SixSeconds, + 7 => D::SevenSeconds, + 8 => D::EightSeconds, + 9 => D::NineSeconds, + 10 => D::TenSeconds, + 11 => D::ElevenSeconds, + 12 => D::TwelveSeconds, + 13 => D::ThirteenSeconds, + 14 => D::FourteenSeconds, + _ => D::FifteenSeconds, + } +} + +fn to_i2v_duration(d: PlanDuration) -> EnqueueKling3p0ProImageToVideoDuration { + use EnqueueKling3p0ProImageToVideoDuration as D; + match d.0 { + 3 => D::ThreeSeconds, + 4 => D::FourSeconds, + 5 => D::FiveSeconds, + 6 => D::SixSeconds, + 7 => D::SevenSeconds, + 8 => D::EightSeconds, + 9 => D::NineSeconds, + 10 => D::TenSeconds, + 11 => D::ElevenSeconds, + 12 => D::TwelveSeconds, + 13 => D::ThirteenSeconds, + 14 => D::FourteenSeconds, + _ => D::FifteenSeconds, + } +} + +fn to_t2v_aspect_ratio(a: PlanAspectRatio) -> EnqueueKling3p0ProTextToVideoAspectRatio { + match a { + PlanAspectRatio::Square => EnqueueKling3p0ProTextToVideoAspectRatio::Square, + PlanAspectRatio::SixteenByNine => EnqueueKling3p0ProTextToVideoAspectRatio::SixteenByNine, + PlanAspectRatio::NineBySixteen => EnqueueKling3p0ProTextToVideoAspectRatio::NineBySixteen, + } +} + +fn to_i2v_aspect_ratio(a: PlanAspectRatio) -> EnqueueKling3p0ProImageToVideoAspectRatio { + match a { + PlanAspectRatio::Square => EnqueueKling3p0ProImageToVideoAspectRatio::Square, + PlanAspectRatio::SixteenByNine => EnqueueKling3p0ProImageToVideoAspectRatio::SixteenByNine, + PlanAspectRatio::NineBySixteen => EnqueueKling3p0ProImageToVideoAspectRatio::NineBySixteen, + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_video_model::CommonVideoModel; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; + + use super::*; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling3p0Pro, + provider: Provider::Fal, + prompt: Some("test".to_string()), + ..Default::default() + } + } + + #[test] + fn no_start_frame_picks_t2v() { + let result = build_fal_kling_3p0_pro(base_builder()).expect("build"); + if let VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling3p0Pro(s)) = result { + assert!(matches!(s.mode, FalKling3p0ProMode::TextToVideo(_))); + } else { panic!("expected FalKling3p0Pro"); } + } + + #[test] + fn end_frame_without_start_frame_errors() { + let mut b = base_builder(); + b.end_frame = Some(ImageRef::Url("https://example.com/end.png".to_string())); + assert!(build_fal_kling_3p0_pro(b).is_err()); + } + + #[test] + fn duration_3s_accepted() { + let mut b = base_builder(); + b.duration_seconds = Some(3); + assert!(build_fal_kling_3p0_pro(b).is_ok()); + } + + #[test] + fn duration_15s_accepted() { + let mut b = base_builder(); + b.duration_seconds = Some(15); + assert!(build_fal_kling_3p0_pro(b).is_ok()); + } + + #[test] + fn duration_out_of_range_errors_with_error_out() { + let mut b = base_builder(); + b.duration_seconds = Some(2); + b.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::ErrorOut; + assert!(build_fal_kling_3p0_pro(b).is_err()); + } + + #[test] + fn duration_out_of_range_clamps_with_pay_less() { + let mut b = base_builder(); + b.duration_seconds = Some(20); + b.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::PayLessDowngrade; + assert!(build_fal_kling_3p0_pro(b).is_ok()); + } + + use crate::generate::generate_video_v2::providers::fal::kling_3p0_pro::request::FalKling3p0ProMode; +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_pro/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_pro/cost.rs new file mode 100644 index 0000000000..cf6d68e1dc --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_pro/cost.rs @@ -0,0 +1,136 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_3p0_pro_image_to_video_webhook::EnqueueKling3p0ProImageToVideoDuration; +use fal_client::requests::webhook::video::text::enqueue_kling_3p0_pro_text_to_video_webhook::EnqueueKling3p0ProTextToVideoDuration; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video_v2::providers::fal::kling_3p0_pro::request::{ + FalKling3p0ProMode, FalKling3p0ProRequestState, +}; + +#[derive(Clone, Debug)] +pub struct FalKling3p0ProCostState { + pub duration_seconds: u64, + pub generate_audio: bool, +} + +impl FalKling3p0ProCostState { + pub fn from_request(request: &FalKling3p0ProRequestState) -> Self { + let (duration_seconds, generate_audio) = match &request.mode { + FalKling3p0ProMode::TextToVideo(req) => ( + t2v_duration_seconds(req.duration), + req.generate_audio.unwrap_or(true), + ), + FalKling3p0ProMode::ImageToVideo(req) => ( + i2v_duration_seconds(req.duration), + req.generate_audio.unwrap_or(true), + ), + }; + Self { duration_seconds, generate_audio } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + // Mirrors fal_client kling_3p0_pro: + // audio off: $0.224/sec (rate=224 in tenths-of-cents) + // audio on: $0.336/sec (rate=336) + // ceiling-divided to whole cents. + let rate: u64 = if self.generate_audio { 336 } else { 224 }; + let cost_in_usd_cents = (rate * self.duration_seconds + 9) / 10; + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + 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, + } + } +} + +/// Default duration is 5s when None per Fal client. +fn t2v_duration_seconds(d: Option) -> u64 { + use EnqueueKling3p0ProTextToVideoDuration as D; + match d { + None => 5, + Some(D::ThreeSeconds) => 3, + Some(D::FourSeconds) => 4, + Some(D::FiveSeconds) => 5, + Some(D::SixSeconds) => 6, + Some(D::SevenSeconds) => 7, + Some(D::EightSeconds) => 8, + Some(D::NineSeconds) => 9, + Some(D::TenSeconds) => 10, + Some(D::ElevenSeconds) => 11, + Some(D::TwelveSeconds) => 12, + Some(D::ThirteenSeconds) => 13, + Some(D::FourteenSeconds) => 14, + Some(D::FifteenSeconds) => 15, + } +} + +fn i2v_duration_seconds(d: Option) -> u64 { + use EnqueueKling3p0ProImageToVideoDuration as D; + match d { + None => 5, + Some(D::ThreeSeconds) => 3, + Some(D::FourSeconds) => 4, + Some(D::FiveSeconds) => 5, + Some(D::SixSeconds) => 6, + Some(D::SevenSeconds) => 7, + Some(D::EightSeconds) => 8, + Some(D::NineSeconds) => 9, + Some(D::TenSeconds) => 10, + Some(D::ElevenSeconds) => 11, + Some(D::TwelveSeconds) => 12, + Some(D::ThirteenSeconds) => 13, + Some(D::FourteenSeconds) => 14, + Some(D::FifteenSeconds) => 15, + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_video_model::CommonVideoModel; + use crate::api::provider::Provider; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + + fn cost_cents(duration_seconds: Option, generate_audio: Option) -> u64 { + let b = GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling3p0Pro, + provider: Provider::Fal, + prompt: Some("test".to_string()), + duration_seconds, + generate_audio, + ..Default::default() + }; + b.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap() + } + + #[test] + fn audio_on_5s_is_168() { + // rate=336, (336*5 + 9) / 10 = 1689/10 = 168. + assert_eq!(cost_cents(Some(5), Some(true)), 168); + } + + #[test] + fn audio_off_5s_is_112() { + // rate=224, (224*5 + 9) / 10 = 1129/10 = 112. + assert_eq!(cost_cents(Some(5), Some(false)), 112); + } + + #[test] + fn audio_on_10s_is_336() { + assert_eq!(cost_cents(Some(10), Some(true)), 336); + } + + #[test] + fn audio_off_15s_is_336() { + // rate=224, (224*15 + 9) / 10 = 3369/10 = 336. + assert_eq!(cost_cents(Some(15), Some(false)), 336); + } + + #[test] + fn default_duration_is_5s() { + assert_eq!(cost_cents(None, Some(true)), cost_cents(Some(5), Some(true))); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_pro/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_pro/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_pro/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_pro/request.rs new file mode 100644 index 0000000000..58c2adf287 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_pro/request.rs @@ -0,0 +1,63 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use fal_client::requests::webhook::video::image::enqueue_kling_3p0_pro_image_to_video_webhook::{ + enqueue_kling_3p0_pro_image_to_video_webhook, EnqueueKling3p0ProImageToVideoArgs, + EnqueueKling3p0ProImageToVideoRequest, +}; +use fal_client::requests::webhook::video::text::enqueue_kling_3p0_pro_text_to_video_webhook::{ + enqueue_kling_3p0_pro_text_to_video_webhook, EnqueueKling3p0ProTextToVideoArgs, + EnqueueKling3p0ProTextToVideoRequest, +}; + +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; + +#[derive(Clone, Debug)] +pub enum FalKling3p0ProMode { + TextToVideo(EnqueueKling3p0ProTextToVideoRequest), + ImageToVideo(EnqueueKling3p0ProImageToVideoRequest), +} + +#[derive(Clone, Debug)] +pub struct FalKling3p0ProRequestState { + pub mode: FalKling3p0ProMode, +} + +impl FalKling3p0ProRequestState { + pub async fn send(&self, client: &RouterFalClient) -> Result { + let (webhook_response, outbound_request): (_, Arc) = match &self.mode { + FalKling3p0ProMode::TextToVideo(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueKling3p0ProTextToVideoArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_kling_3p0_pro_text_to_video_webhook(args).await, outbound) + } + FalKling3p0ProMode::ImageToVideo(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueKling3p0ProImageToVideoArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_kling_3p0_pro_image_to_video_webhook(args).await, outbound) + } + }; + + let webhook_response = webhook_response + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + maybe_outbound_request: Some(outbound_request), + })) + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_standard/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_standard/build.rs new file mode 100644 index 0000000000..f4acfafa47 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_standard/build.rs @@ -0,0 +1,163 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_3p0_standard_image_to_video_webhook::{ + EnqueueKling3p0StandardImageToVideoAspectRatio, EnqueueKling3p0StandardImageToVideoDuration, + EnqueueKling3p0StandardImageToVideoRequest, +}; +use fal_client::requests::webhook::video::text::enqueue_kling_3p0_standard_text_to_video_webhook::{ + EnqueueKling3p0StandardTextToVideoAspectRatio, EnqueueKling3p0StandardTextToVideoDuration, + EnqueueKling3p0StandardTextToVideoRequest, +}; + +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_v2::providers::fal::kling_1_6_pro::build::optional_url; +use crate::generate::generate_video_v2::providers::fal::kling_3p0_pro::build::{ + plan_aspect_ratio, plan_duration, PlanAspectRatio, PlanDuration, +}; +use crate::generate::generate_video_v2::providers::fal::kling_3p0_standard::request::{ + FalKling3p0StandardMode, FalKling3p0StandardRequestState, +}; +use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + +pub fn build_fal_kling_3p0_standard( + builder: GenerateVideoRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio, strategy)?; + let duration = plan_duration(builder.duration_seconds, strategy)?; + let prompt = builder.prompt.clone().unwrap_or_default(); + let negative_prompt = builder.negative_prompt.clone(); + let generate_audio = builder.generate_audio; + + let mode = match optional_url(builder.start_frame.clone())? { + None => { + if builder.end_frame.is_some() { + return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "end_frame", + value: "Kling 3.0 Standard requires a start_frame when end_frame is provided".to_string(), + })); + } + FalKling3p0StandardMode::TextToVideo(EnqueueKling3p0StandardTextToVideoRequest { + prompt, + generate_audio, + negative_prompt, + duration: duration.map(to_t2v_duration), + aspect_ratio: aspect_ratio.map(to_t2v_aspect_ratio), + shot_type: None, + }) + } + Some(image_url) => FalKling3p0StandardMode::ImageToVideo(EnqueueKling3p0StandardImageToVideoRequest { + prompt, + image_url, + end_image_url: optional_url(builder.end_frame.clone())?, + generate_audio, + negative_prompt, + duration: duration.map(to_i2v_duration), + aspect_ratio: aspect_ratio.map(to_i2v_aspect_ratio), + shot_type: None, + }), + }; + + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling3p0Standard( + FalKling3p0StandardRequestState { mode }, + ))) +} + +fn to_t2v_duration(d: PlanDuration) -> EnqueueKling3p0StandardTextToVideoDuration { + use EnqueueKling3p0StandardTextToVideoDuration as D; + match d.0 { + 3 => D::ThreeSeconds, + 4 => D::FourSeconds, + 5 => D::FiveSeconds, + 6 => D::SixSeconds, + 7 => D::SevenSeconds, + 8 => D::EightSeconds, + 9 => D::NineSeconds, + 10 => D::TenSeconds, + 11 => D::ElevenSeconds, + 12 => D::TwelveSeconds, + 13 => D::ThirteenSeconds, + 14 => D::FourteenSeconds, + _ => D::FifteenSeconds, + } +} + +fn to_i2v_duration(d: PlanDuration) -> EnqueueKling3p0StandardImageToVideoDuration { + use EnqueueKling3p0StandardImageToVideoDuration as D; + match d.0 { + 3 => D::ThreeSeconds, + 4 => D::FourSeconds, + 5 => D::FiveSeconds, + 6 => D::SixSeconds, + 7 => D::SevenSeconds, + 8 => D::EightSeconds, + 9 => D::NineSeconds, + 10 => D::TenSeconds, + 11 => D::ElevenSeconds, + 12 => D::TwelveSeconds, + 13 => D::ThirteenSeconds, + 14 => D::FourteenSeconds, + _ => D::FifteenSeconds, + } +} + +fn to_t2v_aspect_ratio(a: PlanAspectRatio) -> EnqueueKling3p0StandardTextToVideoAspectRatio { + match a { + PlanAspectRatio::Square => EnqueueKling3p0StandardTextToVideoAspectRatio::Square, + PlanAspectRatio::SixteenByNine => EnqueueKling3p0StandardTextToVideoAspectRatio::SixteenByNine, + PlanAspectRatio::NineBySixteen => EnqueueKling3p0StandardTextToVideoAspectRatio::NineBySixteen, + } +} + +fn to_i2v_aspect_ratio(a: PlanAspectRatio) -> EnqueueKling3p0StandardImageToVideoAspectRatio { + match a { + PlanAspectRatio::Square => EnqueueKling3p0StandardImageToVideoAspectRatio::Square, + PlanAspectRatio::SixteenByNine => EnqueueKling3p0StandardImageToVideoAspectRatio::SixteenByNine, + PlanAspectRatio::NineBySixteen => EnqueueKling3p0StandardImageToVideoAspectRatio::NineBySixteen, + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_video_model::CommonVideoModel; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + + use super::*; + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling3p0Standard, + provider: Provider::Fal, + prompt: Some("test".to_string()), + ..Default::default() + } + } + + #[test] + fn no_start_frame_picks_t2v() { + let result = build_fal_kling_3p0_standard(base_builder()).expect("build"); + if let VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling3p0Standard(s)) = result { + assert!(matches!(s.mode, FalKling3p0StandardMode::TextToVideo(_))); + } else { panic!("expected FalKling3p0Standard"); } + } + + #[test] + fn start_frame_picks_i2v() { + let mut b = base_builder(); + b.start_frame = Some(ImageRef::Url("https://example.com/a.png".to_string())); + let result = build_fal_kling_3p0_standard(b).expect("build"); + if let VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling3p0Standard(s)) = result { + assert!(matches!(s.mode, FalKling3p0StandardMode::ImageToVideo(_))); + } else { panic!("expected FalKling3p0Standard"); } + } + + #[test] + fn end_frame_without_start_frame_errors() { + let mut b = base_builder(); + b.end_frame = Some(ImageRef::Url("https://example.com/end.png".to_string())); + assert!(build_fal_kling_3p0_standard(b).is_err()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_standard/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_standard/cost.rs new file mode 100644 index 0000000000..4b1e4bfe29 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_standard/cost.rs @@ -0,0 +1,130 @@ +use fal_client::requests::webhook::video::image::enqueue_kling_3p0_standard_image_to_video_webhook::EnqueueKling3p0StandardImageToVideoDuration; +use fal_client::requests::webhook::video::text::enqueue_kling_3p0_standard_text_to_video_webhook::EnqueueKling3p0StandardTextToVideoDuration; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video_v2::providers::fal::kling_3p0_standard::request::{ + FalKling3p0StandardMode, FalKling3p0StandardRequestState, +}; + +#[derive(Clone, Debug)] +pub struct FalKling3p0StandardCostState { + pub duration_seconds: u64, + pub generate_audio: bool, +} + +impl FalKling3p0StandardCostState { + pub fn from_request(request: &FalKling3p0StandardRequestState) -> Self { + let (duration_seconds, generate_audio) = match &request.mode { + FalKling3p0StandardMode::TextToVideo(req) => ( + t2v_duration_seconds(req.duration), + req.generate_audio.unwrap_or(true), + ), + FalKling3p0StandardMode::ImageToVideo(req) => ( + i2v_duration_seconds(req.duration), + req.generate_audio.unwrap_or(true), + ), + }; + Self { duration_seconds, generate_audio } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + // Mirrors fal_client kling_3p0_standard: + // audio off: $0.168/sec (rate=168) + // audio on: $0.252/sec (rate=252) + let rate: u64 = if self.generate_audio { 252 } else { 168 }; + let cost_in_usd_cents = (rate * self.duration_seconds + 9) / 10; + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + 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, + } + } +} + +/// Default duration is 5s when None per Fal client. +fn t2v_duration_seconds(d: Option) -> u64 { + use EnqueueKling3p0StandardTextToVideoDuration as D; + match d { + None => 5, + Some(D::ThreeSeconds) => 3, + Some(D::FourSeconds) => 4, + Some(D::FiveSeconds) => 5, + Some(D::SixSeconds) => 6, + Some(D::SevenSeconds) => 7, + Some(D::EightSeconds) => 8, + Some(D::NineSeconds) => 9, + Some(D::TenSeconds) => 10, + Some(D::ElevenSeconds) => 11, + Some(D::TwelveSeconds) => 12, + Some(D::ThirteenSeconds) => 13, + Some(D::FourteenSeconds) => 14, + Some(D::FifteenSeconds) => 15, + } +} + +fn i2v_duration_seconds(d: Option) -> u64 { + use EnqueueKling3p0StandardImageToVideoDuration as D; + match d { + None => 5, + Some(D::ThreeSeconds) => 3, + Some(D::FourSeconds) => 4, + Some(D::FiveSeconds) => 5, + Some(D::SixSeconds) => 6, + Some(D::SevenSeconds) => 7, + Some(D::EightSeconds) => 8, + Some(D::NineSeconds) => 9, + Some(D::TenSeconds) => 10, + Some(D::ElevenSeconds) => 11, + Some(D::TwelveSeconds) => 12, + Some(D::ThirteenSeconds) => 13, + Some(D::FourteenSeconds) => 14, + Some(D::FifteenSeconds) => 15, + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_video_model::CommonVideoModel; + use crate::api::provider::Provider; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + + fn cost_cents(duration_seconds: Option, generate_audio: Option) -> u64 { + let b = GenerateVideoRequestBuilder { + model: CommonVideoModel::Kling3p0Standard, + provider: Provider::Fal, + prompt: Some("test".to_string()), + duration_seconds, + generate_audio, + ..Default::default() + }; + b.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap() + } + + #[test] + fn audio_on_5s_is_126() { + // rate=252, (252*5 + 9) / 10 = 1269/10 = 126. + assert_eq!(cost_cents(Some(5), Some(true)), 126); + } + + #[test] + fn audio_off_5s_is_84() { + // rate=168, (168*5 + 9) / 10 = 849/10 = 84. + assert_eq!(cost_cents(Some(5), Some(false)), 84); + } + + #[test] + fn audio_on_10s_is_252() { + assert_eq!(cost_cents(Some(10), Some(true)), 252); + } + + #[test] + fn audio_off_15s_is_252() { + // (168*15 + 9) / 10 = 2529/10 = 252. + assert_eq!(cost_cents(Some(15), Some(false)), 252); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_standard/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_standard/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_standard/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_standard/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_standard/request.rs new file mode 100644 index 0000000000..d13ea663cf --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/kling_3p0_standard/request.rs @@ -0,0 +1,63 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use fal_client::requests::webhook::video::image::enqueue_kling_3p0_standard_image_to_video_webhook::{ + enqueue_kling_3p0_standard_image_to_video_webhook, EnqueueKling3p0StandardImageToVideoArgs, + EnqueueKling3p0StandardImageToVideoRequest, +}; +use fal_client::requests::webhook::video::text::enqueue_kling_3p0_standard_text_to_video_webhook::{ + enqueue_kling_3p0_standard_text_to_video_webhook, EnqueueKling3p0StandardTextToVideoArgs, + EnqueueKling3p0StandardTextToVideoRequest, +}; + +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; + +#[derive(Clone, Debug)] +pub enum FalKling3p0StandardMode { + TextToVideo(EnqueueKling3p0StandardTextToVideoRequest), + ImageToVideo(EnqueueKling3p0StandardImageToVideoRequest), +} + +#[derive(Clone, Debug)] +pub struct FalKling3p0StandardRequestState { + pub mode: FalKling3p0StandardMode, +} + +impl FalKling3p0StandardRequestState { + pub async fn send(&self, client: &RouterFalClient) -> Result { + let (webhook_response, outbound_request): (_, Arc) = match &self.mode { + FalKling3p0StandardMode::TextToVideo(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueKling3p0StandardTextToVideoArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_kling_3p0_standard_text_to_video_webhook(args).await, outbound) + } + FalKling3p0StandardMode::ImageToVideo(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueKling3p0StandardImageToVideoArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_kling_3p0_standard_image_to_video_webhook(args).await, outbound) + } + }; + + let webhook_response = webhook_response + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + maybe_outbound_request: Some(outbound_request), + })) + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/mod.rs index 412dc9b6ff..2e3bea7db6 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/fal/mod.rs @@ -1,3 +1,10 @@ +pub mod kling_1_6_pro; +pub mod kling_2_1_master; +pub mod kling_2_1_pro; +pub mod kling_2_5_turbo_pro; +pub mod kling_2_6_pro; +pub mod kling_3p0_pro; +pub mod kling_3p0_standard; pub mod seedance_1p0_lite; pub mod seedance_1p5_pro; pub mod veo_2; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs index e55ee1694e..a1e4ceb51d 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs @@ -23,6 +23,20 @@ use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp::co use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp::request::ArtcraftSeedance2p0BytePlusRequestState; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp_fast::cost::ArtcraftSeedance2p0BytePlusFastCostState; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp_fast::request::ArtcraftSeedance2p0BytePlusFastRequestState; +use crate::generate::generate_video_v2::providers::fal::kling_1_6_pro::cost::FalKling16ProCostState; +use crate::generate::generate_video_v2::providers::fal::kling_1_6_pro::request::FalKling16ProRequestState; +use crate::generate::generate_video_v2::providers::fal::kling_2_1_master::cost::FalKling21MasterCostState; +use crate::generate::generate_video_v2::providers::fal::kling_2_1_master::request::FalKling21MasterRequestState; +use crate::generate::generate_video_v2::providers::fal::kling_2_1_pro::cost::FalKling21ProCostState; +use crate::generate::generate_video_v2::providers::fal::kling_2_1_pro::request::FalKling21ProRequestState; +use crate::generate::generate_video_v2::providers::fal::kling_2_5_turbo_pro::cost::FalKling2p5TurboProCostState; +use crate::generate::generate_video_v2::providers::fal::kling_2_5_turbo_pro::request::FalKling2p5TurboProRequestState; +use crate::generate::generate_video_v2::providers::fal::kling_2_6_pro::cost::FalKling2p6ProCostState; +use crate::generate::generate_video_v2::providers::fal::kling_2_6_pro::request::FalKling2p6ProRequestState; +use crate::generate::generate_video_v2::providers::fal::kling_3p0_pro::cost::FalKling3p0ProCostState; +use crate::generate::generate_video_v2::providers::fal::kling_3p0_pro::request::FalKling3p0ProRequestState; +use crate::generate::generate_video_v2::providers::fal::kling_3p0_standard::cost::FalKling3p0StandardCostState; +use crate::generate::generate_video_v2::providers::fal::kling_3p0_standard::request::FalKling3p0StandardRequestState; use crate::generate::generate_video_v2::providers::fal::seedance_1p0_lite::cost::FalSeedance10LiteCostState; use crate::generate::generate_video_v2::providers::fal::seedance_1p0_lite::request::FalSeedance10LiteRequestState; use crate::generate::generate_video_v2::providers::fal::seedance_1p5_pro::cost::FalSeedance1p5ProCostState; @@ -62,6 +76,13 @@ pub enum VideoGenerationRequest { ArtcraftSeedance2p0UltraFast(ArtcraftSeedance2p0UltraFastRequestState), ArtcraftSeedance2p0BytePlus(ArtcraftSeedance2p0BytePlusRequestState), ArtcraftSeedance2p0BytePlusFast(ArtcraftSeedance2p0BytePlusFastRequestState), + FalKling16Pro(FalKling16ProRequestState), + FalKling21Master(FalKling21MasterRequestState), + FalKling21Pro(FalKling21ProRequestState), + FalKling2p5TurboPro(FalKling2p5TurboProRequestState), + FalKling2p6Pro(FalKling2p6ProRequestState), + FalKling3p0Pro(FalKling3p0ProRequestState), + FalKling3p0Standard(FalKling3p0StandardRequestState), FalSeedance10Lite(FalSeedance10LiteRequestState), FalSeedance1p5Pro(FalSeedance1p5ProRequestState), FalVeo2(FalVeo2RequestState), @@ -91,6 +112,13 @@ impl VideoGenerationRequest { Self::ArtcraftSeedance2p0UltraFast(_) => Provider::Artcraft, Self::ArtcraftSeedance2p0BytePlus(_) => Provider::Artcraft, Self::ArtcraftSeedance2p0BytePlusFast(_) => Provider::Artcraft, + Self::FalKling16Pro(_) => Provider::Fal, + Self::FalKling21Master(_) => Provider::Fal, + Self::FalKling21Pro(_) => Provider::Fal, + Self::FalKling2p5TurboPro(_) => Provider::Fal, + Self::FalKling2p6Pro(_) => Provider::Fal, + Self::FalKling3p0Pro(_) => Provider::Fal, + Self::FalKling3p0Standard(_) => Provider::Fal, Self::FalSeedance10Lite(_) => Provider::Fal, Self::FalSeedance1p5Pro(_) => Provider::Fal, Self::FalVeo2(_) => Provider::Fal, @@ -120,6 +148,13 @@ impl VideoGenerationRequest { 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::FalKling16Pro(request) => Ok(FalKling16ProCostState::from_request(request).estimate_cost()), + VideoGenerationRequest::FalKling21Master(request) => Ok(FalKling21MasterCostState::from_request(request).estimate_cost()), + VideoGenerationRequest::FalKling21Pro(request) => Ok(FalKling21ProCostState::from_request(request).estimate_cost()), + VideoGenerationRequest::FalKling2p5TurboPro(request) => Ok(FalKling2p5TurboProCostState::from_request(request).estimate_cost()), + VideoGenerationRequest::FalKling2p6Pro(request) => Ok(FalKling2p6ProCostState::from_request(request).estimate_cost()), + VideoGenerationRequest::FalKling3p0Pro(request) => Ok(FalKling3p0ProCostState::from_request(request).estimate_cost()), + VideoGenerationRequest::FalKling3p0Standard(request) => Ok(FalKling3p0StandardCostState::from_request(request).estimate_cost()), VideoGenerationRequest::FalSeedance10Lite(request) => Ok(FalSeedance10LiteCostState::from_request(request).estimate_cost()), VideoGenerationRequest::FalSeedance1p5Pro(request) => Ok(FalSeedance1p5ProCostState::from_request(request).estimate_cost()), VideoGenerationRequest::FalVeo2(request) => Ok(FalVeo2CostState::from_request(request).estimate_cost()), @@ -180,6 +215,34 @@ impl VideoGenerationRequest { let client_ref = client.get_artcraft_client_ref()?; request.send(client_ref).await }, + VideoGenerationRequest::FalKling16Pro(request) => { + let client_ref = client.get_fal_client_ref()?; + request.send(client_ref).await + }, + VideoGenerationRequest::FalKling21Master(request) => { + let client_ref = client.get_fal_client_ref()?; + request.send(client_ref).await + }, + VideoGenerationRequest::FalKling21Pro(request) => { + let client_ref = client.get_fal_client_ref()?; + request.send(client_ref).await + }, + VideoGenerationRequest::FalKling2p5TurboPro(request) => { + let client_ref = client.get_fal_client_ref()?; + request.send(client_ref).await + }, + VideoGenerationRequest::FalKling2p6Pro(request) => { + let client_ref = client.get_fal_client_ref()?; + request.send(client_ref).await + }, + VideoGenerationRequest::FalKling3p0Pro(request) => { + let client_ref = client.get_fal_client_ref()?; + request.send(client_ref).await + }, + VideoGenerationRequest::FalKling3p0Standard(request) => { + let client_ref = client.get_fal_client_ref()?; + request.send(client_ref).await + }, VideoGenerationRequest::FalSeedance10Lite(request) => { let client_ref = client.get_fal_client_ref()?; request.send(client_ref).await