kling models

This commit is contained in:
Brandon Thomas committed 2026-05-26 05:44:39 -04:00
1 parent 74d1ccfa6c
commit f6e22b5179
32 files changed
+2575

No files matched your search

@@ -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),
@@ -287,6 +287,320 @@ fn v2_cost(builder: GenerateVideoRequestBuilder) -> Option<u64> {
.and_then(|estimate| estimate.cost_in_usd_cents)
}
// ── Kling shared helpers ──
mod kling_helpers {
use super::*;
pub fn kling_aspect_ratios() -> &'static [Option<CommonAspectRatio>] {
&[
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 {
@@ -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<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
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<ImageRef>,
field: &'static str,
reason: &str,
) -> Result<String, ArtcraftRouterError> {
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<ImageRef>) -> Result<Option<String>, 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<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Kling1p6ProAspectRatio, ArtcraftRouterError> {
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<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Kling1p6ProDuration, ArtcraftRouterError> {
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);
}
}
@@ -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<u16>) -> 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); }
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -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<GenerateVideoResponse, ArtcraftRouterError> {
let outbound_request: Arc<dyn Debug + Send + Sync> = 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),
}))
}
}
@@ -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<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
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<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Kling2p1MasterAspectRatio, ArtcraftRouterError> {
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<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Kling2p1MasterDuration, ArtcraftRouterError> {
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());
}
}
@@ -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<u16>) -> 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); }
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -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<GenerateVideoResponse, ArtcraftRouterError> {
let outbound_request: Arc<dyn Debug + Send + Sync> = 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),
}))
}
}
@@ -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<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
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<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Kling2p1ProAspectRatio, ArtcraftRouterError> {
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<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Kling2p1ProDuration, ArtcraftRouterError> {
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),
},
}
}
@@ -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<u16>) -> 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); }
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -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<GenerateVideoResponse, ArtcraftRouterError> {
let outbound_request: Arc<dyn Debug + Send + Sync> = 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),
}))
}
}
@@ -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<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
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<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<PlanAspectRatio>, 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<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<PlanDuration>, 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());
}
}
@@ -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<u16>, 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); }
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -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<GenerateVideoResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request): (_, Arc<dyn Debug + Send + Sync>) = match &self.mode {
FalKling2p5TurboProMode::TextToVideo(request) => {
let outbound: Arc<dyn Debug + Send + Sync> = 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<dyn Debug + Send + Sync> = 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),
}))
}
}
@@ -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<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
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<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<PlanAspectRatio>, 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<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<PlanDuration>, 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"); }
}
}
@@ -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<u16>, generate_audio: Option<bool>) -> 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)));
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -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<GenerateVideoResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request): (_, Arc<dyn Debug + Send + Sync>) = match &self.mode {
FalKling2p6ProMode::TextToVideo(request) => {
let outbound: Arc<dyn Debug + Send + Sync> = 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<dyn Debug + Send + Sync> = 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),
}))
}
}
@@ -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<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
let state = build_kling_3p0_pro_state(builder)?;
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalKling3p0Pro(state)))
}
fn build_kling_3p0_pro_state(
builder: GenerateVideoRequestBuilder,
) -> Result<FalKling3p0ProRequestState, ArtcraftRouterError> {
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<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<PlanAspectRatio>, 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<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<PlanDuration>, 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;
}
@@ -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<EnqueueKling3p0ProTextToVideoDuration>) -> 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<EnqueueKling3p0ProImageToVideoDuration>) -> 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<u16>, generate_audio: Option<bool>) -> 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)));
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -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<GenerateVideoResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request): (_, Arc<dyn Debug + Send + Sync>) = match &self.mode {
FalKling3p0ProMode::TextToVideo(request) => {
let outbound: Arc<dyn Debug + Send + Sync> = 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<dyn Debug + Send + Sync> = 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),
}))
}
}
@@ -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<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
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());
}
}
@@ -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<EnqueueKling3p0StandardTextToVideoDuration>) -> 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<EnqueueKling3p0StandardImageToVideoDuration>) -> 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<u16>, generate_audio: Option<bool>) -> 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);
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -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<GenerateVideoResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request): (_, Arc<dyn Debug + Send + Sync>) = match &self.mode {
FalKling3p0StandardMode::TextToVideo(request) => {
let outbound: Arc<dyn Debug + Send + Sync> = 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<dyn Debug + Send + Sync> = 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),
}))
}
}
@@ -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;
@@ -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