diff --git a/crates/lib/api_clients/fal_client/src/requests/webhook/video/enqueue_kling_21_master_image_to_video_webhook.rs b/crates/lib/api_clients/fal_client/src/requests/webhook/video/enqueue_kling_21_master_image_to_video_webhook.rs new file mode 100644 index 0000000000..bdd88d07fe --- /dev/null +++ b/crates/lib/api_clients/fal_client/src/requests/webhook/video/enqueue_kling_21_master_image_to_video_webhook.rs @@ -0,0 +1,98 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::kling_video::v2_1::master::image_to_video::{image_to_video, MasterImageToVideoRequest}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct Kling21MasterArgs<'a, U: IntoUrl, V: IntoUrl> { + pub image_url: U, + pub webhook_url: V, + pub prompt: &'a str, + pub api_key: &'a FalApiKey, + pub duration: Kling21MasterDuration, + pub aspect_ratio: Kling21MasterAspectRatio, +} + +#[derive(Copy, Clone, Debug)] +pub enum Kling21MasterDuration { + Default, + FiveSeconds, + TenSeconds, +} + +#[derive(Copy, Clone, Debug)] +pub enum Kling21MasterAspectRatio { + Square, // 1:1 + WideSixteenNine, // 16:9 + TallNineSixteen, // 9:16 +} + +pub async fn enqueue_kling_21_master_image_to_video_webhook( + args: Kling21MasterArgs<'_, U, V> +) -> Result { + let duration = match args.duration { + Kling21MasterDuration::Default => None, + Kling21MasterDuration::FiveSeconds => Some("5".to_string()), // Gross... + Kling21MasterDuration::TenSeconds => Some("10".to_string()), + }; + + let aspect_ratio = match args.aspect_ratio { + Kling21MasterAspectRatio::Square => Some("1:1".to_string()), + Kling21MasterAspectRatio::WideSixteenNine => Some("16:9".to_string()), + Kling21MasterAspectRatio::TallNineSixteen => Some("9:16".to_string()), + }; + + let image_url = args.image_url.as_str().to_string(); + + let request = MasterImageToVideoRequest { + image_url, + prompt: args.prompt.to_string(), + aspect_ratio, + duration, + // Maybe expose these later + cfg_scale: None, + negative_prompt: None, + tail_image_url: None, + }; + + let result = image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::enqueue_kling_21_master_image_to_video_webhook::{enqueue_kling_21_master_image_to_video_webhook, Kling21MasterArgs, Kling21MasterAspectRatio, Kling21MasterDuration}; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] + async fn test_kling21_master_video() -> AnyhowResult<()> { + let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png"; + + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = Kling21MasterArgs { + image_url: image_url, + prompt: "a shot of the mountains, the camera rotates around to show the mountain range, the sun begins to set, and the moon becomes visible", + api_key: &api_key, + duration: Kling21MasterDuration::Default, + aspect_ratio: Kling21MasterAspectRatio::WideSixteenNine, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_kling_21_master_image_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/lib/api_clients/fal_client/src/requests/webhook/video/enqueue_kling_20_pro_image_to_video_webhook.rs b/crates/lib/api_clients/fal_client/src/requests/webhook/video/enqueue_kling_21_pro_image_to_video_webhook.rs similarity index 96% rename from crates/lib/api_clients/fal_client/src/requests/webhook/video/enqueue_kling_20_pro_image_to_video_webhook.rs rename to crates/lib/api_clients/fal_client/src/requests/webhook/video/enqueue_kling_21_pro_image_to_video_webhook.rs index 7424308c9d..6fc471584b 100644 --- a/crates/lib/api_clients/fal_client/src/requests/webhook/video/enqueue_kling_20_pro_image_to_video_webhook.rs +++ b/crates/lib/api_clients/fal_client/src/requests/webhook/video/enqueue_kling_21_pro_image_to_video_webhook.rs @@ -68,13 +68,13 @@ pub async fn enqueue_kling_21_pro_image_to_video_webhook #[cfg(test)] mod tests { use crate::creds::fal_api_key::FalApiKey; - use crate::requests::webhook::video::enqueue_kling_20_pro_image_to_video_webhook::{enqueue_kling_21_pro_image_to_video_webhook, Kling21ProArgs, Kling21ProAspectRatio, Kling21ProDuration}; + use crate::requests::webhook::video::enqueue_kling_21_pro_image_to_video_webhook::{enqueue_kling_21_pro_image_to_video_webhook, Kling21ProArgs, Kling21ProAspectRatio, Kling21ProDuration}; use errors::AnyhowResult; use std::fs::read_to_string; #[tokio::test] #[ignore] - async fn test_Kling21_video() -> AnyhowResult<()> { + async fn test_kling21_pro_video() -> AnyhowResult<()> { let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png"; // XXX: Don't commit secrets! diff --git a/crates/lib/api_clients/fal_client/src/requests/webhook/video/mod.rs b/crates/lib/api_clients/fal_client/src/requests/webhook/video/mod.rs index d94aea082c..c250a5441d 100644 --- a/crates/lib/api_clients/fal_client/src/requests/webhook/video/mod.rs +++ b/crates/lib/api_clients/fal_client/src/requests/webhook/video/mod.rs @@ -1,2 +1,3 @@ pub mod enqueue_kling_16_pro_image_to_video_webhook; -pub mod enqueue_kling_20_pro_image_to_video_webhook; \ No newline at end of file +pub mod enqueue_kling_21_master_image_to_video_webhook; +pub mod enqueue_kling_21_pro_image_to_video_webhook; \ No newline at end of file diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_1/master/image_to_video/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_1/master/image_to_video/mod.rs new file mode 100644 index 0000000000..0711f8898c --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_1/master/image_to_video/mod.rs @@ -0,0 +1,241 @@ +#[allow(unused_imports)] +use crate::prelude::*; +#[allow(unused_imports)] +use serde::{Deserialize, Serialize}; +#[allow(unused_imports)] +use std::collections::HashMap; + +// #[derive(Debug, Serialize, Deserialize, Default)] +// pub struct CameraControl { +// /// The type of camera movement +// /// "horizontal" +// pub movement_type: String, +// /// The value of the camera movement +// /// 10 +// pub movement_value: i64, +// } + +// #[derive(Debug, Serialize, Deserialize, Default)] +// pub struct DynamicMask { +// /// URL of the image for Dynamic Brush Application Area (Mask image created by users using the motion brush) +// /// "https://h2.inkwai.com/bs2/upload-ylab-stunt/ai_portal/1732888130/WU8spl23dA/dynamic_mask_1.png" +// pub mask_url: String, +// /// List of trajectories +// /// [{"x":279,"y":219},{"x":417,"y":65}] +// #[serde(skip_serializing_if = "Option::is_none")] +// pub trajectories: Option>>, +// } + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct File { + /// The mime type of the file. + /// "image/png" + #[serde(skip_serializing_if = "Option::is_none")] + pub content_type: Option, + /// File data + #[serde(skip_serializing_if = "Option::is_none")] + pub file_data: Option, + /// The name of the file. It will be auto-generated if not provided. + /// "z9RV14K95DvU.png" + #[serde(skip_serializing_if = "Option::is_none")] + pub file_name: Option, + /// The size of the file in bytes. + /// 4404019 + #[serde(skip_serializing_if = "Option::is_none")] + pub file_size: Option, + /// The URL where the file can be downloaded from. + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct HTTPValidationError { + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option>>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct I2VOutput { + /// The generated video + /// {"url":"https://v2.fal.media/files/36087878b0c1435bb75c19b64b7db178_output.mp4"} + pub video: File, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ImageToVideoRequest { + /// The aspect ratio of the generated video frame + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + /// The CFG (Classifier Free Guidance) scale is a measure of how close you want + /// the model to stick to your prompt. + #[serde(skip_serializing_if = "Option::is_none")] + pub cfg_scale: Option, + /// The duration of the generated video in seconds + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + pub image_url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + pub prompt: String, +} + +// #[derive(Debug, Serialize, Deserialize, Default)] +// pub struct KlingV1I2VOutput { +// /// The generated video +// /// {"url":"https://v3.fal.media/files/kangaroo/VUmAU3JvzS7mxwdgSU9zj_output.mp4"} +// pub video: File, +// } + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct MasterImageToVideoRequest { + /// The aspect ratio of the generated video frame + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + /// The CFG (Classifier Free Guidance) scale is a measure of how close you want + /// the model to stick to your prompt. + #[serde(skip_serializing_if = "Option::is_none")] + pub cfg_scale: Option, + /// The duration of the generated video in seconds + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + pub image_url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + pub prompt: String, + /// URL of the image to be used for the end of the video + #[serde(skip_serializing_if = "Option::is_none")] + pub tail_image_url: Option, +} + +// #[derive(Debug, Serialize, Deserialize, Default)] +// pub struct T2VOutput { +// /// The generated video +// /// {"url":"https://v2.fal.media/files/fb33a862b94d4d7195e610e4cbc5d392_output.mp4"} +// pub video: File, +// } +// +// #[derive(Debug, Serialize, Deserialize, Default)] +// pub struct TextToVideoRequest { +// /// The aspect ratio of the generated video frame +// #[serde(skip_serializing_if = "Option::is_none")] +// pub aspect_ratio: Option, +// /// The CFG (Classifier Free Guidance) scale is a measure of how close you want +// /// the model to stick to your prompt. +// #[serde(skip_serializing_if = "Option::is_none")] +// pub cfg_scale: Option, +// /// The duration of the generated video in seconds +// #[serde(skip_serializing_if = "Option::is_none")] +// pub duration: Option, +// #[serde(skip_serializing_if = "Option::is_none")] +// pub negative_prompt: Option, +// pub prompt: String, +// } +// +// #[derive(Debug, Serialize, Deserialize, Default)] +// pub struct Trajectory { +// /// X coordinate of the motion trajectory +// /// 279 +// pub x: i64, +// /// Y coordinate of the motion trajectory +// /// 219 +// pub y: i64, +// } +// +// #[derive(Debug, Serialize, Deserialize, Default)] +// pub struct V1ImageToVideoRequest { +// /// The aspect ratio of the generated video frame +// #[serde(skip_serializing_if = "Option::is_none")] +// pub aspect_ratio: Option, +// /// The CFG (Classifier Free Guidance) scale is a measure of how close you want +// /// the model to stick to your prompt. +// #[serde(skip_serializing_if = "Option::is_none")] +// pub cfg_scale: Option, +// /// The duration of the generated video in seconds +// #[serde(skip_serializing_if = "Option::is_none")] +// pub duration: Option, +// /// List of dynamic masks +// #[serde(skip_serializing_if = "Option::is_none")] +// pub dynamic_masks: Option>>, +// /// URL of the image to be used for the video +// /// "https://h2.inkwai.com/bs2/upload-ylab-stunt/se/ai_portal_queue_mmu_image_upscale_aiweb/3214b798-e1b4-4b00-b7af-72b5b0417420_raw_image_0.jpg" +// pub image_url: String, +// #[serde(skip_serializing_if = "Option::is_none")] +// pub negative_prompt: Option, +// /// The prompt for the video +// /// "The astronaut stood up and walked away" +// pub prompt: String, +// /// URL of the image for Static Brush Application Area (Mask image created by users using the motion brush) +// /// "https://h2.inkwai.com/bs2/upload-ylab-stunt/ai_portal/1732888177/cOLNrShrSO/static_mask.png" +// #[serde(skip_serializing_if = "Option::is_none")] +// pub static_mask_url: Option, +// /// URL of the image to be used for the end of the video +// #[serde(skip_serializing_if = "Option::is_none")] +// pub tail_image_url: Option, +// } +// +// #[derive(Debug, Serialize, Deserialize, Default)] +// pub struct V1TextToVideoRequest { +// /// Advanced Camera control parameters +// #[serde(skip_serializing_if = "Option::is_none")] +// pub advanced_camera_control: Option>, +// /// The aspect ratio of the generated video frame +// #[serde(skip_serializing_if = "Option::is_none")] +// pub aspect_ratio: Option, +// /// Camera control parameters +// #[serde(skip_serializing_if = "Option::is_none")] +// pub camera_control: Option, +// /// The CFG (Classifier Free Guidance) scale is a measure of how close you want +// /// the model to stick to your prompt. +// #[serde(skip_serializing_if = "Option::is_none")] +// pub cfg_scale: Option, +// /// The duration of the generated video in seconds +// #[serde(skip_serializing_if = "Option::is_none")] +// pub duration: Option, +// #[serde(skip_serializing_if = "Option::is_none")] +// pub negative_prompt: Option, +// pub prompt: String, +// } +// +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ValidationError { + pub loc: Vec, + pub msg: String, + #[serde(rename = "type")] + pub ty: String, +} +// +// #[derive(Debug, Serialize, Deserialize, Default)] +// pub struct VideoEffectsOutput { +// /// The generated video +// /// {"content_type":"video/mp4","file_name":"output.mp4","url":"https://storage.googleapis.com/falserverless/kling/kling_ex.mp4.mp4"} +// pub video: File, +// } +// +// #[derive(Debug, Serialize, Deserialize, Default)] +// pub struct VideoEffectsRequest { +// /// The duration of the generated video in seconds +// #[serde(skip_serializing_if = "Option::is_none")] +// pub duration: Option, +// /// The effect scene to use for the video generation +// /// "hug" +// pub effect_scene: String, +// /// URL of the image to be used for the squish and expansion video +// /// "https://h2.inkwai.com/bs2/upload-ylab-stunt/se/ai_portal_queue_mmu_image_upscale_aiweb/3214b798-e1b4-4b00-b7af-72b5b0417420_raw_image_0.jpg" +// pub image_url: String, +// /// URL of images to be used for hug, kiss or heart_gesture video. +// /// ["https://storage.googleapis.com/falserverless/juggernaut_examples/VHXMavzPyI27zi6JseyL4.png","https://storage.googleapis.com/falserverless/juggernaut_examples/QEW5VrzccxGva7mPfEXjf.png"] +// #[serde(skip_serializing_if = "Option::is_none")] +// pub input_image_urls: Option>>, +// } + +/// Kling 1.0 +/// +/// Category: text-to-video +/// Machine Type: A100 +/// +/// +/// Kling 1.6 (pro) Image to Video API. +pub fn image_to_video( + params: MasterImageToVideoRequest, +) -> FalRequest { + FalRequest::new("fal-ai/kling-video/v2.1/master/image-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_1/master/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_1/master/mod.rs new file mode 100644 index 0000000000..30605e2d08 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_1/master/mod.rs @@ -0,0 +1,14 @@ +#[cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_kling-video" +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_kling-video" + ))) +)] +pub mod image_to_video; diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_1/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_1/mod.rs index 3c053502e2..1e3f4ca7c1 100644 --- a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_1/mod.rs +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_1/mod.rs @@ -11,4 +11,18 @@ feature = "endpoints_fal-ai_kling-video" ))) )] +pub mod master; +#[cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_kling-video" +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_kling-video" + ))) +)] pub mod pro; \ No newline at end of file