kling 3.0 i2v

This commit is contained in:
Brandon Thomas committed 2026-05-11 18:29:25 -04:00
1 parent 09e197d19a
commit 8a2c4dc0e8
5 files changed
+459

No files matched your search

@@ -0,0 +1,294 @@
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::api::video::image::kling_3p0_standard_image_to_video::raw_request::{
Kling3p0StandardImageToVideoInput, Kling3p0StandardImageToVideoOutput,
};
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
#[derive(Clone, Debug)]
pub struct Kling3p0StandardImageToVideoRequest {
/// Text prompt describing the video to generate.
pub prompt: String,
/// Starting frame image URL.
pub image_url: String,
/// Optional end frame image URL.
pub end_image_url: Option<String>,
/// Whether to generate audio. Defaults to true if not specified.
pub generate_audio: Option<bool>,
/// Optional negative prompt.
pub negative_prompt: Option<String>,
/// Video duration.
pub duration: Option<Kling3p0StandardImageToVideoDuration>,
/// Aspect ratio.
pub aspect_ratio: Option<Kling3p0StandardImageToVideoAspectRatio>,
/// Shot type for multi-shot generation.
pub shot_type: Option<Kling3p0StandardImageToVideoShotType>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Kling3p0StandardImageToVideoDuration {
ThreeSeconds,
FourSeconds,
FiveSeconds,
SixSeconds,
SevenSeconds,
EightSeconds,
NineSeconds,
TenSeconds,
ElevenSeconds,
TwelveSeconds,
ThirteenSeconds,
FourteenSeconds,
FifteenSeconds,
}
impl Kling3p0StandardImageToVideoDuration {
pub fn to_seconds(&self) -> u64 {
match self {
Self::ThreeSeconds => 3,
Self::FourSeconds => 4,
Self::FiveSeconds => 5,
Self::SixSeconds => 6,
Self::SevenSeconds => 7,
Self::EightSeconds => 8,
Self::NineSeconds => 9,
Self::TenSeconds => 10,
Self::ElevenSeconds => 11,
Self::TwelveSeconds => 12,
Self::ThirteenSeconds => 13,
Self::FourteenSeconds => 14,
Self::FifteenSeconds => 15,
}
}
fn to_str(&self) -> &'static str {
match self {
Self::ThreeSeconds => "3",
Self::FourSeconds => "4",
Self::FiveSeconds => "5",
Self::SixSeconds => "6",
Self::SevenSeconds => "7",
Self::EightSeconds => "8",
Self::NineSeconds => "9",
Self::TenSeconds => "10",
Self::ElevenSeconds => "11",
Self::TwelveSeconds => "12",
Self::ThirteenSeconds => "13",
Self::FourteenSeconds => "14",
Self::FifteenSeconds => "15",
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Kling3p0StandardImageToVideoAspectRatio {
Square,
SixteenByNine,
NineBySixteen,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Kling3p0StandardImageToVideoShotType {
Customize,
Intelligent,
}
impl FalEndpoint for Kling3p0StandardImageToVideoRequest {
const ENDPOINT: &str = "fal-ai/kling-video/v3/standard/image-to-video";
type RawRequest = Kling3p0StandardImageToVideoInput;
type RawResponse = Kling3p0StandardImageToVideoOutput;
fn to_raw_request(&self) -> Result<Self::RawRequest, FalErrorPlus> {
let duration = self.duration.map(|d| d.to_str().to_string());
let aspect_ratio = self.aspect_ratio.map(|ar| match ar {
Kling3p0StandardImageToVideoAspectRatio::Square => "1:1",
Kling3p0StandardImageToVideoAspectRatio::SixteenByNine => "16:9",
Kling3p0StandardImageToVideoAspectRatio::NineBySixteen => "9:16",
}.to_string());
let shot_type = self.shot_type.map(|st| match st {
Kling3p0StandardImageToVideoShotType::Customize => "customize",
Kling3p0StandardImageToVideoShotType::Intelligent => "intelligent",
}.to_string());
Ok(Self::RawRequest {
prompt: self.prompt.clone(),
image_url: self.image_url.clone(),
end_image_url: self.end_image_url.clone(),
generate_audio: self.generate_audio,
negative_prompt: self.negative_prompt.clone(),
duration,
aspect_ratio,
shot_type,
cfg_scale: None,
})
}
}
#[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::TREX_SKELETON_IMAGE_URL;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_image_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 = Kling3p0StandardImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking toward the camera".to_string(),
duration: Some(Kling3p0StandardImageToVideoDuration::FiveSeconds),
aspect_ratio: Some(Kling3p0StandardImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(true),
negative_prompt: None,
end_image_url: None,
shot_type: None,
};
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_image_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 = Kling3p0StandardImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton comes alive and roars at the camera".to_string(),
duration: Some(Kling3p0StandardImageToVideoDuration::ThreeSeconds),
aspect_ratio: Some(Kling3p0StandardImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(false),
negative_prompt: None,
end_image_url: None,
shot_type: None,
};
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 = [
Kling3p0StandardImageToVideoAspectRatio::Square,
Kling3p0StandardImageToVideoAspectRatio::SixteenByNine,
Kling3p0StandardImageToVideoAspectRatio::NineBySixteen,
];
for ar in aspect_ratios {
println!("--- aspect ratio: {:?} ---", ar);
let request = Kling3p0StandardImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton comes alive and roars at the camera".to_string(),
duration: Some(Kling3p0StandardImageToVideoDuration::ThreeSeconds),
aspect_ratio: Some(ar),
generate_audio: Some(false),
negative_prompt: None,
end_image_url: None,
shot_type: None,
};
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 = [
Kling3p0StandardImageToVideoDuration::ThreeSeconds,
Kling3p0StandardImageToVideoDuration::FourSeconds,
Kling3p0StandardImageToVideoDuration::FiveSeconds,
Kling3p0StandardImageToVideoDuration::SixSeconds,
Kling3p0StandardImageToVideoDuration::SevenSeconds,
Kling3p0StandardImageToVideoDuration::EightSeconds,
Kling3p0StandardImageToVideoDuration::NineSeconds,
Kling3p0StandardImageToVideoDuration::TenSeconds,
Kling3p0StandardImageToVideoDuration::ElevenSeconds,
Kling3p0StandardImageToVideoDuration::TwelveSeconds,
Kling3p0StandardImageToVideoDuration::ThirteenSeconds,
Kling3p0StandardImageToVideoDuration::FourteenSeconds,
Kling3p0StandardImageToVideoDuration::FifteenSeconds,
];
for dur in durations {
println!("--- duration: {:?} ---", dur);
let request = Kling3p0StandardImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton slowly turns its head and opens its jaw".to_string(),
duration: Some(dur),
aspect_ratio: Some(Kling3p0StandardImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(false),
negative_prompt: None,
end_image_url: None,
shot_type: None,
};
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_shot_types() -> AnyhowResult<()> {
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let shot_types = [
Kling3p0StandardImageToVideoShotType::Customize,
Kling3p0StandardImageToVideoShotType::Intelligent,
];
for st in shot_types {
println!("--- shot type: {:?} ---", st);
let request = Kling3p0StandardImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton lurches forward dramatically".to_string(),
duration: Some(Kling3p0StandardImageToVideoDuration::FiveSeconds),
aspect_ratio: Some(Kling3p0StandardImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(true),
negative_prompt: None,
end_image_url: None,
shot_type: Some(st),
};
let result = request.send_webhook_request(&api_key, "https://example.com/webhook").await?;
println!("result: {:?}", result);
}
Ok(())
}
// NB: Pricing tests are in cost.rs
}
@@ -0,0 +1,110 @@
use crate::requests::api::video::image::kling_3p0_standard_image_to_video::api::{
Kling3p0StandardImageToVideoDuration, Kling3p0StandardImageToVideoRequest,
};
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
impl FalRequestCostCalculator for Kling3p0StandardImageToVideoRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Same pricing as text-to-video for Kling 3.0 Standard:
// Audio off: $0.168/second
// Audio on: $0.252/second
let generate_audio = self.generate_audio.unwrap_or(true);
let duration_secs = self.duration
.unwrap_or(Kling3p0StandardImageToVideoDuration::FiveSeconds)
.to_seconds();
// Rate in tenths-of-cents per second, rounded up
let rate = if generate_audio { 252u64 } else { 168u64 };
(rate * duration_secs + 9) / 10
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::requests::api::video::image::kling_3p0_standard_image_to_video::api::{
Kling3p0StandardImageToVideoAspectRatio, Kling3p0StandardImageToVideoDuration,
};
fn make_request(
generate_audio: Option<bool>,
duration: Option<Kling3p0StandardImageToVideoDuration>,
) -> Kling3p0StandardImageToVideoRequest {
Kling3p0StandardImageToVideoRequest {
prompt: "test".to_string(),
image_url: "https://example.com/image.jpg".to_string(),
end_image_url: None,
generate_audio,
negative_prompt: None,
duration,
aspect_ratio: Some(Kling3p0StandardImageToVideoAspectRatio::SixteenByNine),
shot_type: None,
}
}
mod audio_off {
use super::*;
// Audio off: $0.168/sec → rate = 168 tenths-of-cents/sec
#[test]
fn five_seconds() {
// (168 * 5 + 9) / 10 = 84
assert_eq!(make_request(Some(false), Some(Kling3p0StandardImageToVideoDuration::FiveSeconds)).calculate_cost_in_cents(), 84);
}
#[test]
fn ten_seconds() {
// (168 * 10 + 9) / 10 = 168
assert_eq!(make_request(Some(false), Some(Kling3p0StandardImageToVideoDuration::TenSeconds)).calculate_cost_in_cents(), 168);
}
#[test]
fn fifteen_seconds() {
// (168 * 15 + 9) / 10 = 252
assert_eq!(make_request(Some(false), Some(Kling3p0StandardImageToVideoDuration::FifteenSeconds)).calculate_cost_in_cents(), 252);
}
}
mod audio_on {
use super::*;
// Audio on: $0.252/sec → rate = 252 tenths-of-cents/sec
#[test]
fn five_seconds() {
// (252 * 5 + 9) / 10 = 126
assert_eq!(make_request(Some(true), Some(Kling3p0StandardImageToVideoDuration::FiveSeconds)).calculate_cost_in_cents(), 126);
}
#[test]
fn ten_seconds() {
// (252 * 10 + 9) / 10 = 252
assert_eq!(make_request(Some(true), Some(Kling3p0StandardImageToVideoDuration::TenSeconds)).calculate_cost_in_cents(), 252);
}
}
mod defaults {
use super::*;
#[test]
fn default_audio_is_on() {
// generate_audio=None defaults to true (audio on)
// duration=5s: (252 * 5 + 9) / 10 = 126
assert_eq!(make_request(None, Some(Kling3p0StandardImageToVideoDuration::FiveSeconds)).calculate_cost_in_cents(), 126);
}
#[test]
fn default_duration_is_five_seconds() {
// duration=None defaults to 5s
// audio off: (168 * 5 + 9) / 10 = 84
assert_eq!(make_request(Some(false), None).calculate_cost_in_cents(), 84);
}
#[test]
fn both_defaults() {
// audio on + 5s: (252 * 5 + 9) / 10 = 126
assert_eq!(make_request(None, None).calculate_cost_in_cents(), 126);
}
}
}
@@ -0,0 +1,3 @@
pub mod api;
pub mod cost;
pub mod raw_request;
@@ -0,0 +1,51 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Kling3p0StandardImageToVideoInput {
pub prompt: String,
/// Starting frame image URL.
pub image_url: String,
/// Optional end frame image URL.
#[serde(skip_serializing_if = "Option::is_none")]
pub end_image_url: Option<String>,
/// Aspect ratio.
/// Possible values: "16:9", "9:16", "1:1"
#[serde(skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<String>,
/// Whether to generate audio alongside the video.
#[serde(skip_serializing_if = "Option::is_none")]
pub generate_audio: Option<bool>,
/// Optional negative prompt.
#[serde(skip_serializing_if = "Option::is_none")]
pub negative_prompt: Option<String>,
/// Duration in seconds (as a string).
/// Options: "3" through "15"
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<String>,
/// Shot type for multi-shot video generation.
/// Possible values: "customize", "intelligent"
#[serde(skip_serializing_if = "Option::is_none")]
pub shot_type: Option<String>,
/// The CFG (Classifier Free Guidance) scale.
/// Default value: 0.5
#[serde(skip_serializing_if = "Option::is_none")]
pub cfg_scale: Option<f32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Kling3p0StandardImageToVideoVideoFile {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Kling3p0StandardImageToVideoOutput {
pub video: Kling3p0StandardImageToVideoVideoFile,
}
@@ -0,0 +1 @@
pub mod kling_3p0_standard_image_to_video;