From dcd44573e3541fe10ac2997fa11746b16265bb86 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 1 Jun 2026 17:34:01 -0400 Subject: [PATCH] elements --- .../kling_1p6_pro_elements_to_video/api.rs | 257 +++++++++++++++++ .../kling_1p6_pro_elements_to_video/cost.rs | 261 ++++++++++++++++++ .../kling_1p6_pro_elements_to_video/mod.rs | 3 + .../raw_request.rs | 37 +++ .../src/requests/api/video/elements/mod.rs | 1 + .../fal_client/src/requests/api/video/mod.rs | 1 + 6 files changed, 560 insertions(+) create mode 100644 crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/api.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/cost.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/mod.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/raw_request.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/video/elements/mod.rs diff --git a/crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/api.rs b/crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/api.rs new file mode 100644 index 0000000000..c27c95e6cb --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/api.rs @@ -0,0 +1,257 @@ +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::api::video::elements::kling_1p6_pro_elements_to_video::raw_request::{ + Kling1p6ProElementsToVideoInput, Kling1p6ProElementsToVideoOutput, +}; +use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + +#[derive(Clone, Debug)] +pub struct Kling1p6ProElementsToVideoRequest { + /// Text prompt describing the video to generate. + pub prompt: String, + + /// Image URLs that drive the generation. fal documents support for up + /// to 4 images; passing more is rejected upstream rather than here. + pub input_image_urls: Vec, + + /// Optional negative prompt. fal's default is + /// `"blur, distort, and low quality"` when this is `None`. + pub negative_prompt: Option, + + /// Video duration. Kling 1.6 Pro Elements supports 5 or 10 seconds only. + pub duration: Option, + + /// Aspect ratio. + pub aspect_ratio: Option, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Kling1p6ProElementsToVideoDuration { + FiveSeconds, + TenSeconds, +} + +impl Kling1p6ProElementsToVideoDuration { + pub fn to_seconds(&self) -> u64 { + match self { + Self::FiveSeconds => 5, + Self::TenSeconds => 10, + } + } + + fn to_str(&self) -> &'static str { + match self { + Self::FiveSeconds => "5", + Self::TenSeconds => "10", + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Kling1p6ProElementsToVideoAspectRatio { + Square, + SixteenByNine, + NineBySixteen, +} + +impl FalEndpoint for Kling1p6ProElementsToVideoRequest { + const ENDPOINT: &str = "fal-ai/kling-video/v1.6/pro/elements"; + + type RawRequest = Kling1p6ProElementsToVideoInput; + type RawResponse = Kling1p6ProElementsToVideoOutput; + + fn to_raw_request(&self) -> Result { + let duration = self.duration.map(|d| d.to_str().to_string()); + + let aspect_ratio = self.aspect_ratio.map(|ar| match ar { + Kling1p6ProElementsToVideoAspectRatio::Square => "1:1", + Kling1p6ProElementsToVideoAspectRatio::SixteenByNine => "16:9", + Kling1p6ProElementsToVideoAspectRatio::NineBySixteen => "9:16", + }.to_string()); + + Ok(Self::RawRequest { + prompt: self.prompt.clone(), + input_image_urls: self.input_image_urls.clone(), + aspect_ratio, + duration, + negative_prompt: self.negative_prompt.clone(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::{JUNO_AT_LAKE_IMAGE_URL, TALL_MOCHI_WITH_GLASSES_IMAGE_URL}; + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn test_elements_to_video_webhook() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = Kling1p6ProElementsToVideoRequest { + prompt: "the two dogs run side by side across a sunlit meadow".to_string(), + input_image_urls: vec![ + TALL_MOCHI_WITH_GLASSES_IMAGE_URL.to_string(), + JUNO_AT_LAKE_IMAGE_URL.to_string(), + ], + negative_prompt: None, + duration: Some(Kling1p6ProElementsToVideoDuration::FiveSeconds), + aspect_ratio: Some(Kling1p6ProElementsToVideoAspectRatio::SixteenByNine), + }; + + let result = request.send_webhook_request(&api_key, "https://example.com/webhook").await?; + println!("Webhook result: {:?}", result); + assert!(result.request_id.is_some() || result.gateway_request_id.is_some()); + Ok(()) + } + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn test_elements_to_video_queue() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = Kling1p6ProElementsToVideoRequest { + prompt: "the dogs play together in a snowy field".to_string(), + input_image_urls: vec![ + TALL_MOCHI_WITH_GLASSES_IMAGE_URL.to_string(), + JUNO_AT_LAKE_IMAGE_URL.to_string(), + ], + negative_prompt: None, + duration: Some(Kling1p6ProElementsToVideoDuration::FiveSeconds), + aspect_ratio: Some(Kling1p6ProElementsToVideoAspectRatio::SixteenByNine), + }; + + let result = request.send_queue_request(&api_key).await?; + println!("Queue result — request_id: {}", result.request_id); + assert!(!result.request_id.is_empty()); + Ok(()) + } + + #[tokio::test] + #[ignore] // manually run — fires a real API request per variant (expensive) + async fn test_all_aspect_ratios() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let aspect_ratios = [ + Kling1p6ProElementsToVideoAspectRatio::Square, + Kling1p6ProElementsToVideoAspectRatio::SixteenByNine, + Kling1p6ProElementsToVideoAspectRatio::NineBySixteen, + ]; + + for ar in aspect_ratios { + println!("--- aspect ratio: {:?} ---", ar); + let request = Kling1p6ProElementsToVideoRequest { + prompt: "the dogs play together in a snowy field".to_string(), + input_image_urls: vec![ + TALL_MOCHI_WITH_GLASSES_IMAGE_URL.to_string(), + JUNO_AT_LAKE_IMAGE_URL.to_string(), + ], + negative_prompt: None, + duration: Some(Kling1p6ProElementsToVideoDuration::FiveSeconds), + aspect_ratio: Some(ar), + }; + let result = request.send_webhook_request(&api_key, "https://example.com/webhook").await?; + println!("result: {:?}", result); + } + + Ok(()) + } + + #[tokio::test] + #[ignore] // manually run — fires a real API request per variant (expensive) + async fn test_all_durations() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let durations = [ + Kling1p6ProElementsToVideoDuration::FiveSeconds, + Kling1p6ProElementsToVideoDuration::TenSeconds, + ]; + + for dur in durations { + println!("--- duration: {:?} ---", dur); + let request = Kling1p6ProElementsToVideoRequest { + prompt: "the dogs leap through tall grass in golden hour light".to_string(), + input_image_urls: vec![ + TALL_MOCHI_WITH_GLASSES_IMAGE_URL.to_string(), + JUNO_AT_LAKE_IMAGE_URL.to_string(), + ], + negative_prompt: None, + duration: Some(dur), + aspect_ratio: Some(Kling1p6ProElementsToVideoAspectRatio::SixteenByNine), + }; + let result = request.send_webhook_request(&api_key, "https://example.com/webhook").await?; + println!("result: {:?}", result); + } + + Ok(()) + } + + // ── Wire-shape sanity ── + + #[test] + fn raw_request_aspect_ratio_uses_colon_format() { + let request = Kling1p6ProElementsToVideoRequest { + prompt: "p".to_string(), + input_image_urls: vec!["https://example.com/a.png".to_string()], + negative_prompt: None, + duration: Some(Kling1p6ProElementsToVideoDuration::TenSeconds), + aspect_ratio: Some(Kling1p6ProElementsToVideoAspectRatio::Square), + }; + let raw = request.to_raw_request().unwrap(); + assert_eq!(raw.aspect_ratio.as_deref(), Some("1:1")); + assert_eq!(raw.duration.as_deref(), Some("10")); + } + + #[test] + fn raw_request_passes_through_input_image_urls() { + let urls = vec![ + "https://example.com/a.png".to_string(), + "https://example.com/b.png".to_string(), + "https://example.com/c.png".to_string(), + "https://example.com/d.png".to_string(), + ]; + let request = Kling1p6ProElementsToVideoRequest { + prompt: "p".to_string(), + input_image_urls: urls.clone(), + negative_prompt: None, + duration: None, + aspect_ratio: None, + }; + let raw = request.to_raw_request().unwrap(); + assert_eq!(raw.input_image_urls, urls); + } + + #[test] + fn raw_request_omits_unset_optionals() { + let request = Kling1p6ProElementsToVideoRequest { + prompt: "p".to_string(), + input_image_urls: vec!["https://example.com/a.png".to_string()], + negative_prompt: None, + duration: None, + aspect_ratio: None, + }; + let raw = request.to_raw_request().unwrap(); + assert_eq!(raw.prompt, "p"); + assert!(raw.aspect_ratio.is_none()); + assert!(raw.duration.is_none()); + assert!(raw.negative_prompt.is_none()); + } + + #[test] + fn endpoint_path_is_canonical() { + assert_eq!( + Kling1p6ProElementsToVideoRequest::ENDPOINT, + "fal-ai/kling-video/v1.6/pro/elements", + ); + } + + // NB: Pricing tests are in cost.rs +} diff --git a/crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/cost.rs b/crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/cost.rs new file mode 100644 index 0000000000..dbc20023c1 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/cost.rs @@ -0,0 +1,261 @@ +use crate::requests::api::video::elements::kling_1p6_pro_elements_to_video::api::{ + Kling1p6ProElementsToVideoDuration, Kling1p6ProElementsToVideoRequest, +}; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; + +impl FalRequestCostCalculator for Kling1p6ProElementsToVideoRequest { + fn calculate_cost_in_cents(&self) -> UsdCents { + // Kling 1.6 Pro elements-to-video: $0.098/second + // (see https://fal.ai/models/fal-ai/kling-video/v1.6/pro/elements). + // + // Same per-second rate as the v1.6 Pro text-to-video variant. Rate held + // in tenths-of-cents and rounded up to whole cents at the end so the + // user is never undercharged. + let duration_secs = self.duration + .unwrap_or(Kling1p6ProElementsToVideoDuration::FiveSeconds) + .to_seconds(); + (98u64 * duration_secs + 9) / 10 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::requests::api::video::elements::kling_1p6_pro_elements_to_video::api::{ + Kling1p6ProElementsToVideoAspectRatio, Kling1p6ProElementsToVideoDuration, + }; + + fn make_request( + duration: Option, + ) -> Kling1p6ProElementsToVideoRequest { + Kling1p6ProElementsToVideoRequest { + prompt: "test".to_string(), + input_image_urls: vec!["https://example.com/a.png".to_string()], + negative_prompt: None, + duration, + aspect_ratio: Some(Kling1p6ProElementsToVideoAspectRatio::SixteenByNine), + } + } + + // Pricing: $0.098/sec = 98 tenths-of-cents/sec. + // Cents per duration = ceil(98 × secs / 10). + + #[test] + fn five_seconds() { + // (98 * 5 + 9) / 10 = 49 + assert_eq!( + make_request(Some(Kling1p6ProElementsToVideoDuration::FiveSeconds)) + .calculate_cost_in_cents(), + 49, + ); + } + + #[test] + fn ten_seconds() { + // (98 * 10 + 9) / 10 = 98 + assert_eq!( + make_request(Some(Kling1p6ProElementsToVideoDuration::TenSeconds)) + .calculate_cost_in_cents(), + 98, + ); + } + + #[test] + fn default_duration_is_five_seconds() { + // duration=None → 5s → 49¢ + assert_eq!(make_request(None).calculate_cost_in_cents(), 49); + } + + #[test] + fn ten_seconds_costs_more_than_five() { + let five = make_request(Some(Kling1p6ProElementsToVideoDuration::FiveSeconds)) + .calculate_cost_in_cents(); + let ten = make_request(Some(Kling1p6ProElementsToVideoDuration::TenSeconds)) + .calculate_cost_in_cents(); + assert!(five < ten, "five={five}¢ < ten={ten}¢"); + } + + #[test] + fn input_image_count_does_not_affect_cost() { + // fal bills per second of output, not per input image. Build matched + // pairs differing only in input_image_urls length and confirm cost + // matches exactly. + let base = Kling1p6ProElementsToVideoRequest { + prompt: "p".to_string(), + input_image_urls: vec!["https://example.com/a.png".to_string()], + negative_prompt: None, + duration: Some(Kling1p6ProElementsToVideoDuration::FiveSeconds), + aspect_ratio: Some(Kling1p6ProElementsToVideoAspectRatio::SixteenByNine), + }; + let baseline_cost = base.calculate_cost_in_cents(); + for n in 1..=4usize { + let urls: Vec = (0..n).map(|i| format!("https://example.com/{i}.png")).collect(); + let req = Kling1p6ProElementsToVideoRequest { input_image_urls: urls, ..base.clone() }; + assert_eq!( + req.calculate_cost_in_cents(), + baseline_cost, + "image count {n} should not change cost", + ); + } + } + + /// Exhaustive cost-table tests: every permutation of cost-relevant + /// configuration for elements-to-video. There is no direct legacy + /// equivalent (the legacy `enqueue_kling_v1p6_pro_image_to_video_webhook` + /// is image-to-video at a slightly lower rate), so the parity here is + /// against the canonical $0.098/sec formula. A separate sub-test + /// cross-references the legacy image-to-video function to confirm the + /// expected per-duration premium ($0.003/sec). + mod cost_table { + use super::*; + use crate::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator; + use crate::requests_old::webhook::video::image::enqueue_kling_v1p6_pro_image_to_video_webhook::{ + Kling1p6ProAspectRatio as LegacyAspectRatio, + Kling1p6ProDuration as LegacyDuration, + Kling1p6ProRequest as LegacyRequest, + }; + + fn new_request( + duration: Option, + aspect_ratio: Option, + ) -> Kling1p6ProElementsToVideoRequest { + Kling1p6ProElementsToVideoRequest { + prompt: "test".to_string(), + input_image_urls: vec!["https://example.com/a.png".to_string()], + negative_prompt: None, + duration, + aspect_ratio, + } + } + + // (duration, aspect_ratio, expected_cents) + // + // Math: ceil(98 × secs / 10) where 5s → 49¢ and 10s → 98¢. + // duration=None defaults to 5s. + // Aspect ratio is irrelevant to cost; iterated only to assert it. + const COST_TABLE: &[( + Option, + Option, + u64, + )] = &[ + // duration=None → 5s default → 49¢ + (None, None, 49), + (None, Some(Kling1p6ProElementsToVideoAspectRatio::Square), 49), + (None, Some(Kling1p6ProElementsToVideoAspectRatio::SixteenByNine), 49), + (None, Some(Kling1p6ProElementsToVideoAspectRatio::NineBySixteen), 49), + // duration=5s → 49¢ + (Some(Kling1p6ProElementsToVideoDuration::FiveSeconds), None, 49), + (Some(Kling1p6ProElementsToVideoDuration::FiveSeconds), Some(Kling1p6ProElementsToVideoAspectRatio::Square), 49), + (Some(Kling1p6ProElementsToVideoDuration::FiveSeconds), Some(Kling1p6ProElementsToVideoAspectRatio::SixteenByNine), 49), + (Some(Kling1p6ProElementsToVideoDuration::FiveSeconds), Some(Kling1p6ProElementsToVideoAspectRatio::NineBySixteen), 49), + // duration=10s → 98¢ + (Some(Kling1p6ProElementsToVideoDuration::TenSeconds), None, 98), + (Some(Kling1p6ProElementsToVideoDuration::TenSeconds), Some(Kling1p6ProElementsToVideoAspectRatio::Square), 98), + (Some(Kling1p6ProElementsToVideoDuration::TenSeconds), Some(Kling1p6ProElementsToVideoAspectRatio::SixteenByNine), 98), + (Some(Kling1p6ProElementsToVideoDuration::TenSeconds), Some(Kling1p6ProElementsToVideoAspectRatio::NineBySixteen), 98), + ]; + + #[test] + fn new_module_matches_cost_table() { + for &(duration, aspect_ratio, expected) in COST_TABLE { + let got = new_request(duration, aspect_ratio).calculate_cost_in_cents(); + assert_eq!( + got, expected, + "new module: duration={duration:?} aspect_ratio={aspect_ratio:?}", + ); + } + } + + /// Aspect ratio is not part of the billing formula — every row in the + /// cost table must produce the same value for any aspect-ratio choice. + #[test] + fn cost_is_independent_of_aspect_ratio() { + let aspect_ratios = [ + None, + Some(Kling1p6ProElementsToVideoAspectRatio::Square), + Some(Kling1p6ProElementsToVideoAspectRatio::SixteenByNine), + Some(Kling1p6ProElementsToVideoAspectRatio::NineBySixteen), + ]; + let durations = [ + None, + Some(Kling1p6ProElementsToVideoDuration::FiveSeconds), + Some(Kling1p6ProElementsToVideoDuration::TenSeconds), + ]; + for duration in durations { + let baseline = new_request(duration, aspect_ratios[0]).calculate_cost_in_cents(); + for &ar in &aspect_ratios[1..] { + let cost = new_request(duration, ar).calculate_cost_in_cents(); + assert_eq!(cost, baseline, "duration={duration:?} ar={ar:?}"); + } + } + } + + /// Cross-check against the legacy image-to-video function. Elements-to-video + /// is `$0.098/sec` and the legacy image-to-video is `$0.095/sec`, so at + /// each duration the cost difference equals `ceil(0.003 × secs)`: + /// 5s: 49¢ − 48¢ = 1¢ + /// 10s: 98¢ − 95¢ = 3¢ + /// If a future change to the legacy image-to-video rate breaks this + /// invariant, this test calls it out. + #[test] + fn premium_over_legacy_image_to_video_is_as_expected() { + fn legacy_image(secs_variant: LegacyDuration) -> u64 { + LegacyRequest { + image_url: "https://example.com/i.png".to_string(), + end_frame_image_url: None, + prompt: "test".to_string(), + duration: secs_variant, + aspect_ratio: LegacyAspectRatio::WideSixteenNine, + }.calculate_cost_in_cents() + } + + // (elements duration, equivalent legacy image duration, expected premium in cents) + let cases = [ + (Kling1p6ProElementsToVideoDuration::FiveSeconds, LegacyDuration::FiveSeconds, 1u64), + (Kling1p6ProElementsToVideoDuration::TenSeconds, LegacyDuration::TenSeconds, 3u64), + ]; + + for (elements_d, legacy_d, expected_premium) in cases { + let elements_cost = new_request(Some(elements_d), None).calculate_cost_in_cents(); + let image_cost = legacy_image(legacy_d); + let premium = elements_cost.saturating_sub(image_cost); + assert_eq!( + premium, expected_premium, + "{elements_d:?}: elements={elements_cost}¢ legacy_image={image_cost}¢ premium={premium}¢ (want {expected_premium}¢)", + ); + } + } + + /// Elements and text-to-video share the same $0.098/sec rate — cells + /// should match exactly at every duration. Documents the rate-equality + /// expectation, so if either rate drifts in the future this test fires. + #[test] + fn matches_text_to_video_cost_at_each_duration() { + use crate::requests::api::video::text::kling_1p6_pro_text_to_video::api::{ + Kling1p6ProTextToVideoAspectRatio, Kling1p6ProTextToVideoDuration, + Kling1p6ProTextToVideoRequest, + }; + fn t2v(duration: Kling1p6ProTextToVideoDuration) -> u64 { + Kling1p6ProTextToVideoRequest { + prompt: "test".to_string(), + negative_prompt: None, + duration: Some(duration), + aspect_ratio: Some(Kling1p6ProTextToVideoAspectRatio::SixteenByNine), + cfg_scale: None, + }.calculate_cost_in_cents() + } + let pairs = [ + (Kling1p6ProElementsToVideoDuration::FiveSeconds, Kling1p6ProTextToVideoDuration::FiveSeconds), + (Kling1p6ProElementsToVideoDuration::TenSeconds, Kling1p6ProTextToVideoDuration::TenSeconds), + ]; + for (e_d, t_d) in pairs { + let elements_cost = new_request(Some(e_d), None).calculate_cost_in_cents(); + let text_cost = t2v(t_d); + assert_eq!( + elements_cost, text_cost, + "{e_d:?} vs {t_d:?}: elements={elements_cost}¢ text={text_cost}¢ — rates should match at $0.098/sec", + ); + } + } + } +} diff --git a/crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/mod.rs b/crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/mod.rs new file mode 100644 index 0000000000..3bfb6c86db --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/mod.rs @@ -0,0 +1,3 @@ +pub mod api; +pub mod cost; +pub mod raw_request; diff --git a/crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/raw_request.rs b/crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/raw_request.rs new file mode 100644 index 0000000000..4e4d7a2e5d --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/video/elements/kling_1p6_pro_elements_to_video/raw_request.rs @@ -0,0 +1,37 @@ +use serde::{Deserialize, Serialize}; + +/// Over-the-wire input shape for `fal-ai/kling-video/v1.6/pro/elements`. +/// fal's schema: +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Kling1p6ProElementsToVideoInput { + pub prompt: String, + + /// List of image URLs that drive the generation. fal documents support + /// for up to 4 images. + pub input_image_urls: Vec, + + /// Aspect ratio. + /// Possible values: "16:9", "9:16", "1:1" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Duration in seconds (as a string). + /// Possible values: "5", "10" + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Optional negative prompt. + /// fal's default: "blur, distort, and low quality" + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Kling1p6ProElementsToVideoVideoFile { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Kling1p6ProElementsToVideoOutput { + pub video: Kling1p6ProElementsToVideoVideoFile, +} diff --git a/crates/api_clients/fal_client/src/requests/api/video/elements/mod.rs b/crates/api_clients/fal_client/src/requests/api/video/elements/mod.rs new file mode 100644 index 0000000000..1dbfd19473 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/video/elements/mod.rs @@ -0,0 +1 @@ +pub mod kling_1p6_pro_elements_to_video; diff --git a/crates/api_clients/fal_client/src/requests/api/video/mod.rs b/crates/api_clients/fal_client/src/requests/api/video/mod.rs index 70e7921855..acc16a8985 100644 --- a/crates/api_clients/fal_client/src/requests/api/video/mod.rs +++ b/crates/api_clients/fal_client/src/requests/api/video/mod.rs @@ -1,2 +1,3 @@ +pub mod elements; pub mod image; pub mod text; \ No newline at end of file