mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
kling 3.0 i2v
This commit is contained in:
1 parent
09e197d19a
commit
8a2c4dc0e8
5 files changed
+459
No files matched your search
+294
@@ -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
|
||||
}
|
||||
+110
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod api;
|
||||
pub mod cost;
|
||||
pub mod raw_request;
|
||||
+51
@@ -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;
|
||||
Reference in new issue
Block a user