mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-25 02:29:37 +00:00
elements
This commit is contained in:
+257
@@ -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<String>,
|
||||
|
||||
/// Optional negative prompt. fal's default is
|
||||
/// `"blur, distort, and low quality"` when this is `None`.
|
||||
pub negative_prompt: Option<String>,
|
||||
|
||||
/// Video duration. Kling 1.6 Pro Elements supports 5 or 10 seconds only.
|
||||
pub duration: Option<Kling1p6ProElementsToVideoDuration>,
|
||||
|
||||
/// Aspect ratio.
|
||||
pub aspect_ratio: Option<Kling1p6ProElementsToVideoAspectRatio>,
|
||||
}
|
||||
|
||||
#[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<Self::RawRequest, FalErrorPlus> {
|
||||
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
|
||||
}
|
||||
+261
@@ -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<Kling1p6ProElementsToVideoDuration>,
|
||||
) -> 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<String> = (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<Kling1p6ProElementsToVideoDuration>,
|
||||
aspect_ratio: Option<Kling1p6ProElementsToVideoAspectRatio>,
|
||||
) -> 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<Kling1p6ProElementsToVideoDuration>,
|
||||
Option<Kling1p6ProElementsToVideoAspectRatio>,
|
||||
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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod api;
|
||||
pub mod cost;
|
||||
pub mod raw_request;
|
||||
+37
@@ -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: <https://fal.ai/models/fal-ai/kling-video/v1.6/pro/elements/api>
|
||||
#[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<String>,
|
||||
|
||||
/// Aspect ratio.
|
||||
/// Possible values: "16:9", "9:16", "1:1"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio: Option<String>,
|
||||
|
||||
/// Duration in seconds (as a string).
|
||||
/// Possible values: "5", "10"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration: Option<String>,
|
||||
|
||||
/// Optional negative prompt.
|
||||
/// fal's default: "blur, distort, and low quality"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub negative_prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Kling1p6ProElementsToVideoVideoFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Kling1p6ProElementsToVideoOutput {
|
||||
pub video: Kling1p6ProElementsToVideoVideoFile,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod kling_1p6_pro_elements_to_video;
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod elements;
|
||||
pub mod image;
|
||||
pub mod text;
|
||||
Reference in New Issue
Block a user