mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 17:58:55 +00:00
kling models
This commit is contained in:
1 parent
74d1ccfa6c
commit
f6e22b5179
32 files changed
+2575
No files matched your search
+21
@@ -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),
|
||||
|
||||
+314
@@ -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 {
|
||||
|
||||
+166
@@ -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);
|
||||
}
|
||||
}
|
||||
+61
@@ -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); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+40
@@ -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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
+123
@@ -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());
|
||||
}
|
||||
}
|
||||
+61
@@ -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); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+40
@@ -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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
+85
@@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
+61
@@ -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); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+40
@@ -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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
+189
@@ -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());
|
||||
}
|
||||
}
|
||||
+75
@@ -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); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+63
@@ -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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
+186
@@ -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"); }
|
||||
}
|
||||
}
|
||||
+88
@@ -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)));
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+63
@@ -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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
+253
@@ -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;
|
||||
}
|
||||
+136
@@ -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)));
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+63
@@ -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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
+163
@@ -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());
|
||||
}
|
||||
}
|
||||
+130
@@ -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);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+63
@@ -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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
+7
@@ -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;
|
||||
|
||||
+63
@@ -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
|
||||
|
||||
Reference in new issue
Block a user