Compare commits

...
Author SHA1 Message Date
Brandon Thomas 22e59eb683 de-vendor 2026-03-08 05:59:38 -04:00
Brandon Thomas 038e1c1895 fix handlers 2026-03-07 09:18:52 -05:00
Brandon Thomas 0daccd4888 wip 2026-03-07 09:04:51 -05:00
Brandon Thomas 1fa26a92e8 sora2 - move vendored fal.rs deps I hacked in back into fal_client 2026-03-06 00:30:13 -05:00
17 changed files with 386 additions and 175 deletions
Generated
+1
View File
@@ -4185,6 +4185,7 @@ dependencies = [
"log",
"reqwest 0.12.23",
"serde",
"strum 0.28.0",
"test_data",
"testing",
"tokio",
+1
View File
@@ -25,6 +25,7 @@ futures.workspace = true
log.workspace = true
reqwest.workspace = true
serde.workspace = true
strum.workspace = true
tokio.workspace = true
url.workspace = true
@@ -1 +1,2 @@
pub mod image;
pub mod video;
@@ -1,44 +1,43 @@
use crate::prelude::{Deserialize, FalRequest, Serialize};
use fal::request::FalRequest;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Sora2ImageToVideoInput {
pub prompt: String,
/// Starting frame
/// Starting frame image URL.
pub image_url: String,
/// Possible enum values: auto, 720p
/// Default value auto
/// Default value: "auto"
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution: Option<String>,
/// Possible enum values: auto, 9:16, 16:9
/// Default value "auto"
/// Default value: "auto"
#[serde(skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<String>,
/// Duration in seconds
/// Duration in seconds.
/// Possible enum values: 4, 8, 12
/// Default value 4
/// Default value: 4
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<u8>,
/// Whether to delete the video after generation for privacy reasons.
/// If True, the video cannot be used for remixing and will be permanently deleted.
/// Default value: true
#[serde(skip_serializing_if = "Option::is_none")]
pub delete_video: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Sora2ImageToVideoOutput {
pub video: VideoFile,
pub struct Sora2ImageToVideoVideoFile {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct VideoFile {
/// The URL where the file can be downloaded from.
pub url: String,
#[derive(Debug, Serialize, Deserialize)]
pub struct Sora2ImageToVideoOutput {
pub video: Sora2ImageToVideoVideoFile,
}
pub fn sora_2_image_to_video(
@@ -1,44 +1,43 @@
use crate::prelude::{Deserialize, FalRequest, Serialize};
use fal::request::FalRequest;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Sora2ProImageToVideoInput {
pub prompt: String,
/// Starting frame
/// Starting frame image URL.
pub image_url: String,
/// Possible enum values: auto, 720p, 1080p
/// Default value auto
/// Default value: "auto"
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution: Option<String>,
/// Possible enum values: auto, 9:16, 16:9
/// Default value "auto"
/// Default value: "auto"
#[serde(skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<String>,
/// Duration in seconds
/// Duration in seconds.
/// Possible enum values: 4, 8, 12
/// Default value 4
/// Default value: 4
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<u8>,
/// Whether to delete the video after generation for privacy reasons.
/// If True, the video cannot be used for remixing and will be permanently deleted.
/// Default value: true
#[serde(skip_serializing_if = "Option::is_none")]
pub delete_video: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Sora2ProImageToVideoOutput {
pub video: VideoFile,
pub struct Sora2ProImageToVideoVideoFile {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct VideoFile {
/// The URL where the file can be downloaded from.
pub url: String,
#[derive(Debug, Serialize, Deserialize)]
pub struct Sora2ProImageToVideoOutput {
pub video: Sora2ProImageToVideoVideoFile,
}
pub fn sora_2_pro_image_to_video(
@@ -1,41 +1,40 @@
use crate::prelude::{Deserialize, FalRequest, Serialize};
use fal::request::FalRequest;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Sora2ProTextToVideoInput {
pub prompt: String,
/// Possible enum values: auto, 720p, 1080p
/// Default value auto
/// Possible enum values: 720p, 1080p
/// Default value: "1080p"
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution: Option<String>,
/// Possible enum values: auto, 9:16, 16:9
/// Default value "auto"
/// Possible enum values: 9:16, 16:9
/// Default value: "16:9"
#[serde(skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<String>,
/// Duration in seconds
/// Duration in seconds.
/// Possible enum values: 4, 8, 12
/// Default value 4
/// Default value: 4
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<u8>,
/// Whether to delete the video after generation for privacy reasons.
/// If True, the video cannot be used for remixing and will be permanently deleted.
/// Default value: true
#[serde(skip_serializing_if = "Option::is_none")]
pub delete_video: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Sora2ProTextToVideoOutput {
pub video: VideoFile,
pub struct Sora2ProTextToVideoVideoFile {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct VideoFile {
/// The URL where the file can be downloaded from.
pub url: String,
#[derive(Debug, Serialize, Deserialize)]
pub struct Sora2ProTextToVideoOutput {
pub video: Sora2ProTextToVideoVideoFile,
}
pub fn sora_2_pro_text_to_video(
@@ -1,41 +1,40 @@
use crate::prelude::{Deserialize, FalRequest, Serialize};
use fal::request::FalRequest;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Sora2TextToVideoInput {
pub prompt: String,
/// Possible enum values: 720p
/// Default value 720p
/// Default value: "720p"
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution: Option<String>,
/// Possible enum values: auto, 9:16, 16:9
/// Default value "auto"
/// Possible enum values: 9:16, 16:9
/// Default value: "16:9"
#[serde(skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<String>,
/// Duration in seconds
/// Duration in seconds.
/// Possible enum values: 4, 8, 12
/// Default value 4
/// Default value: 4
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<u8>,
/// Whether to delete the video after generation for privacy reasons.
/// If True, the video cannot be used for remixing and will be permanently deleted.
/// Default value: true
#[serde(skip_serializing_if = "Option::is_none")]
pub delete_video: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Sora2TextToVideoOutput {
pub video: VideoFile,
pub struct Sora2TextToVideoVideoFile {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct VideoFile {
/// The URL where the file can be downloaded from.
pub url: String,
#[derive(Debug, Serialize, Deserialize)]
pub struct Sora2TextToVideoOutput {
pub video: Sora2TextToVideoVideoFile,
}
pub fn sora_2_text_to_video(
@@ -0,0 +1,4 @@
pub mod http_sora_2_text_to_video;
pub mod http_sora_2_image_to_video;
pub mod http_sora_2_pro_text_to_video;
pub mod http_sora_2_pro_image_to_video;
@@ -1,8 +1,8 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::http::video::http_sora_2_image_to_video::{sora_2_image_to_video, Sora2ImageToVideoInput};
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::sora::sora2::sora_2_image_to_video::{sora_2_image_to_video, Sora2ImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -12,10 +12,7 @@ pub struct EnqueueSora2ImageToVideoArgs<'a, R: IntoUrl> {
pub image_url: String,
// Optional args
// NB: Duration defaults to four seconds
pub duration: Option<EnqueueSora2ImageToVideoDurationSeconds>,
pub resolution: Option<EnqueueSora2ImageToVideoResolution>,
pub aspect_ratio: Option<EnqueueSora2ImageToVideoAspectRatio>,
@@ -24,20 +21,20 @@ pub struct EnqueueSora2ImageToVideoArgs<'a, R: IntoUrl> {
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, strum::EnumIter)]
pub enum EnqueueSora2ImageToVideoDurationSeconds {
Four,
Eight,
Twelve,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, strum::EnumIter)]
pub enum EnqueueSora2ImageToVideoResolution {
Auto,
SevenTwentyP,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, strum::EnumIter)]
pub enum EnqueueSora2ImageToVideoAspectRatio {
Auto,
NineBySixteen,
@@ -49,8 +46,8 @@ impl <U: IntoUrl> FalRequestCostCalculator for EnqueueSora2ImageToVideoArgs<'_,
// "The pricing is $0.1/s for Sora 2."
let duration = self.duration.unwrap_or(EnqueueSora2ImageToVideoDurationSeconds::Four);
match duration {
EnqueueSora2ImageToVideoDurationSeconds::Four => 40, // $0.10 * 4
EnqueueSora2ImageToVideoDurationSeconds::Eight => 80, // $0.10 * 8
EnqueueSora2ImageToVideoDurationSeconds::Four => 40, // $0.10 * 4
EnqueueSora2ImageToVideoDurationSeconds::Eight => 80, // $0.10 * 8
EnqueueSora2ImageToVideoDurationSeconds::Twelve => 120 // $0.10 * 12
}
}
@@ -63,35 +60,33 @@ pub async fn enqueue_sora_2_image_to_video_webhook<R: IntoUrl>(
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = args.duration
.map(|resolution| match resolution {
.map(|d| match d {
EnqueueSora2ImageToVideoDurationSeconds::Four => 4,
EnqueueSora2ImageToVideoDurationSeconds::Eight => 8,
EnqueueSora2ImageToVideoDurationSeconds::Twelve => 12,
});
let resolution = args.resolution
.map(|resolution| match resolution {
.map(|r| match r {
EnqueueSora2ImageToVideoResolution::Auto => "auto",
EnqueueSora2ImageToVideoResolution::SevenTwentyP => "720p",
})
.map(|resolution| resolution.to_string());
.map(|r| r.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
.map(|ar| match ar {
EnqueueSora2ImageToVideoAspectRatio::Auto => "auto",
EnqueueSora2ImageToVideoAspectRatio::NineBySixteen => "9:16",
EnqueueSora2ImageToVideoAspectRatio::SixteenByNine => "16:9",
})
.map(|resolution| resolution.to_string());
.map(|ar| ar.to_string());
let request = Sora2ImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
// Optionals
duration,
resolution,
aspect_ratio,
// Constants
delete_video: Some(false),
};
@@ -105,18 +100,17 @@ pub async fn enqueue_sora_2_image_to_video_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use super::*;
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::image::enqueue_sora_2_image_to_video_webhook::{enqueue_sora_2_image_to_video_webhook, EnqueueSora2ImageToVideoArgs, EnqueueSora2ImageToVideoAspectRatio, EnqueueSora2ImageToVideoDurationSeconds, EnqueueSora2ImageToVideoResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use strum::IntoEnumIterator;
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
#[tokio::test]
#[ignore]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueSora2ImageToVideoArgs {
@@ -130,6 +124,79 @@ mod tests {
};
let result = enqueue_sora_2_image_to_video_webhook(args).await?;
println!("result: {:?}", result);
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);
for ar in EnqueueSora2ImageToVideoAspectRatio::iter() {
println!("--- aspect ratio: {:?} ---", ar);
let args = EnqueueSora2ImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton comes alive and roars at the camera".to_string(),
duration: Some(EnqueueSora2ImageToVideoDurationSeconds::Four),
aspect_ratio: Some(ar),
resolution: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_image_to_video_webhook(args).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);
for dur in EnqueueSora2ImageToVideoDurationSeconds::iter() {
println!("--- duration: {:?} ---", dur);
let args = EnqueueSora2ImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton slowly turns its head".to_string(),
duration: Some(dur),
aspect_ratio: Some(EnqueueSora2ImageToVideoAspectRatio::SixteenByNine),
resolution: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_image_to_video_webhook(args).await?;
println!("result: {:?}", result);
}
Ok(())
}
#[tokio::test]
#[ignore] // manually run — fires a real API request per variant (expensive)
async fn test_all_resolutions() -> AnyhowResult<()> {
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
for res in EnqueueSora2ImageToVideoResolution::iter() {
println!("--- resolution: {:?} ---", res);
let args = EnqueueSora2ImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton slowly comes alive".to_string(),
duration: Some(EnqueueSora2ImageToVideoDurationSeconds::Four),
aspect_ratio: Some(EnqueueSora2ImageToVideoAspectRatio::SixteenByNine),
resolution: Some(res),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_image_to_video_webhook(args).await?;
println!("result: {:?}", result);
}
Ok(())
}
@@ -1,8 +1,8 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::http::video::http_sora_2_pro_image_to_video::{sora_2_pro_image_to_video, Sora2ProImageToVideoInput};
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::sora::sora2::sora_2_pro_image_to_video::{sora_2_pro_image_to_video, Sora2ProImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -21,21 +21,21 @@ pub struct EnqueueSora2ProImageToVideoArgs<'a, R: IntoUrl> {
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, strum::EnumIter)]
pub enum EnqueueSora2ProImageToVideoDurationSeconds {
Four,
Eight,
Twelve,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, strum::EnumIter)]
pub enum EnqueueSora2ProImageToVideoResolution {
Auto,
SevenTwentyP,
TenEightyP,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, strum::EnumIter)]
pub enum EnqueueSora2ProImageToVideoAspectRatio {
Auto,
NineBySixteen,
@@ -44,29 +44,24 @@ pub enum EnqueueSora2ProImageToVideoAspectRatio {
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueSora2ProImageToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "If an OpenAI API key is provided, it will be directly charged by OpenAI.
// Otherwise, you will be charged fal credits.
// The pricing is $0.30/s for 720p and $0.50/s for 1080p."
// NB(bt): There's no way to provide an API key with this endpoint?
// Pricing: $0.30/s for 720p and $0.50/s for 1080p.
let duration = self.duration.unwrap_or(EnqueueSora2ProImageToVideoDurationSeconds::Four);
let resolution = self.resolution.unwrap_or(EnqueueSora2ProImageToVideoResolution::Auto); // WHAT IS THIS?!
let resolution = self.resolution.unwrap_or(EnqueueSora2ProImageToVideoResolution::Auto);
match (duration, resolution) {
(EnqueueSora2ProImageToVideoDurationSeconds::Four, EnqueueSora2ProImageToVideoResolution::Auto) => 120, // TODO: Auto = ???
(EnqueueSora2ProImageToVideoDurationSeconds::Four, EnqueueSora2ProImageToVideoResolution::Auto) => 120,
(EnqueueSora2ProImageToVideoDurationSeconds::Four, EnqueueSora2ProImageToVideoResolution::SevenTwentyP) => 120,
(EnqueueSora2ProImageToVideoDurationSeconds::Four, EnqueueSora2ProImageToVideoResolution::TenEightyP) => 200,
(EnqueueSora2ProImageToVideoDurationSeconds::Eight, EnqueueSora2ProImageToVideoResolution::Auto) => 240, // TODO: Auto = ???
(EnqueueSora2ProImageToVideoDurationSeconds::Eight, EnqueueSora2ProImageToVideoResolution::Auto) => 240,
(EnqueueSora2ProImageToVideoDurationSeconds::Eight, EnqueueSora2ProImageToVideoResolution::SevenTwentyP) => 240,
(EnqueueSora2ProImageToVideoDurationSeconds::Eight, EnqueueSora2ProImageToVideoResolution::TenEightyP) => 400,
(EnqueueSora2ProImageToVideoDurationSeconds::Twelve, EnqueueSora2ProImageToVideoResolution::Auto) => 360, // TODO: Auto = ???
(EnqueueSora2ProImageToVideoDurationSeconds::Twelve, EnqueueSora2ProImageToVideoResolution::Auto) => 360,
(EnqueueSora2ProImageToVideoDurationSeconds::Twelve, EnqueueSora2ProImageToVideoResolution::SevenTwentyP) => 360,
(EnqueueSora2ProImageToVideoDurationSeconds::Twelve, EnqueueSora2ProImageToVideoResolution::TenEightyP) => 600,
}
}
}
/// Sora 2 Pro Image-to-Video
/// https://fal.ai/models/fal-ai/sora-2/image-to-video/pro
pub async fn enqueue_sora_2_pro_image_to_video_webhook<R: IntoUrl>(
@@ -74,36 +69,34 @@ pub async fn enqueue_sora_2_pro_image_to_video_webhook<R: IntoUrl>(
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = args.duration
.map(|resolution| match resolution {
.map(|d| match d {
EnqueueSora2ProImageToVideoDurationSeconds::Four => 4,
EnqueueSora2ProImageToVideoDurationSeconds::Eight => 8,
EnqueueSora2ProImageToVideoDurationSeconds::Twelve => 12,
});
let resolution = args.resolution
.map(|resolution| match resolution {
.map(|r| match r {
EnqueueSora2ProImageToVideoResolution::Auto => "auto",
EnqueueSora2ProImageToVideoResolution::SevenTwentyP => "720p",
EnqueueSora2ProImageToVideoResolution::TenEightyP => "1080p",
})
.map(|resolution| resolution.to_string());
.map(|r| r.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
.map(|ar| match ar {
EnqueueSora2ProImageToVideoAspectRatio::Auto => "auto",
EnqueueSora2ProImageToVideoAspectRatio::NineBySixteen => "9:16",
EnqueueSora2ProImageToVideoAspectRatio::SixteenByNine => "16:9",
})
.map(|resolution| resolution.to_string());
.map(|ar| ar.to_string());
let request = Sora2ProImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
// Optionals
duration,
resolution,
aspect_ratio,
// Constants
delete_video: Some(false),
};
@@ -117,18 +110,17 @@ pub async fn enqueue_sora_2_pro_image_to_video_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use super::*;
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::image::enqueue_sora_2_pro_image_to_video_webhook::{enqueue_sora_2_pro_image_to_video_webhook, EnqueueSora2ProImageToVideoArgs, EnqueueSora2ProImageToVideoAspectRatio, EnqueueSora2ProImageToVideoDurationSeconds, EnqueueSora2ProImageToVideoResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use strum::IntoEnumIterator;
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
#[tokio::test]
#[ignore]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueSora2ProImageToVideoArgs {
@@ -142,6 +134,79 @@ mod tests {
};
let result = enqueue_sora_2_pro_image_to_video_webhook(args).await?;
println!("result: {:?}", result);
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);
for ar in EnqueueSora2ProImageToVideoAspectRatio::iter() {
println!("--- aspect ratio: {:?} ---", ar);
let args = EnqueueSora2ProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton comes alive and roars at the camera".to_string(),
duration: Some(EnqueueSora2ProImageToVideoDurationSeconds::Four),
aspect_ratio: Some(ar),
resolution: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_pro_image_to_video_webhook(args).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);
for dur in EnqueueSora2ProImageToVideoDurationSeconds::iter() {
println!("--- duration: {:?} ---", dur);
let args = EnqueueSora2ProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton slowly turns its head".to_string(),
duration: Some(dur),
aspect_ratio: Some(EnqueueSora2ProImageToVideoAspectRatio::SixteenByNine),
resolution: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_pro_image_to_video_webhook(args).await?;
println!("result: {:?}", result);
}
Ok(())
}
#[tokio::test]
#[ignore] // manually run — fires a real API request per variant (expensive)
async fn test_all_resolutions() -> AnyhowResult<()> {
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
for res in EnqueueSora2ProImageToVideoResolution::iter() {
println!("--- resolution: {:?} ---", res);
let args = EnqueueSora2ProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton slowly comes alive".to_string(),
duration: Some(EnqueueSora2ProImageToVideoDurationSeconds::Four),
aspect_ratio: Some(EnqueueSora2ProImageToVideoAspectRatio::SixteenByNine),
resolution: Some(res),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_pro_image_to_video_webhook(args).await?;
println!("result: {:?}", result);
}
Ok(())
}
@@ -1,8 +1,8 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::http::video::http_sora_2_pro_text_to_video::{sora_2_pro_text_to_video, Sora2ProTextToVideoInput};
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::sora::sora2::sora_2_pro_text_to_video::{sora_2_pro_text_to_video, Sora2ProTextToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -20,23 +20,21 @@ pub struct EnqueueSora2ProTextToVideoArgs<'a, R: IntoUrl> {
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, strum::EnumIter)]
pub enum EnqueueSora2ProTextToVideoDurationSeconds {
Four,
Eight,
Twelve,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, strum::EnumIter)]
pub enum EnqueueSora2ProTextToVideoResolution {
Auto,
SevenTwentyP,
TenEightyP,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, strum::EnumIter)]
pub enum EnqueueSora2ProTextToVideoAspectRatio {
Auto,
NineBySixteen,
SixteenByNine,
}
@@ -44,29 +42,21 @@ pub enum EnqueueSora2ProTextToVideoAspectRatio {
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueSora2ProTextToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "If an OpenAI API key is provided, it will be directly charged by OpenAI.
// Otherwise, you will be charged fal credits.
// The pricing is $0.30/s for 720p and $0.50/s for 1080p."
// NB(bt): There's no way to provide an API key with this endpoint?
// Pricing: $0.30/s for 720p and $0.50/s for 1080p.
let duration = self.duration.unwrap_or(EnqueueSora2ProTextToVideoDurationSeconds::Four);
let resolution = self.resolution.unwrap_or(EnqueueSora2ProTextToVideoResolution::Auto); // WHAT IS THIS?!
let resolution = self.resolution.unwrap_or(EnqueueSora2ProTextToVideoResolution::TenEightyP);
match (duration, resolution) {
(EnqueueSora2ProTextToVideoDurationSeconds::Four, EnqueueSora2ProTextToVideoResolution::Auto) => 120, // TODO: Auto = ???
(EnqueueSora2ProTextToVideoDurationSeconds::Four, EnqueueSora2ProTextToVideoResolution::SevenTwentyP) => 120,
(EnqueueSora2ProTextToVideoDurationSeconds::Four, EnqueueSora2ProTextToVideoResolution::TenEightyP) => 200,
(EnqueueSora2ProTextToVideoDurationSeconds::Eight, EnqueueSora2ProTextToVideoResolution::Auto) => 240, // TODO: Auto = ???
(EnqueueSora2ProTextToVideoDurationSeconds::Eight, EnqueueSora2ProTextToVideoResolution::SevenTwentyP) => 240,
(EnqueueSora2ProTextToVideoDurationSeconds::Eight, EnqueueSora2ProTextToVideoResolution::TenEightyP) => 400,
(EnqueueSora2ProTextToVideoDurationSeconds::Twelve, EnqueueSora2ProTextToVideoResolution::Auto) => 360, // TODO: Auto = ???
(EnqueueSora2ProTextToVideoDurationSeconds::Twelve, EnqueueSora2ProTextToVideoResolution::SevenTwentyP) => 360,
(EnqueueSora2ProTextToVideoDurationSeconds::Twelve, EnqueueSora2ProTextToVideoResolution::TenEightyP) => 600,
}
}
}
/// Sora 2 Pro Text-to-Video
/// https://fal.ai/models/fal-ai/sora-2/text-to-video/pro
pub async fn enqueue_sora_2_pro_text_to_video_webhook<R: IntoUrl>(
@@ -74,35 +64,31 @@ pub async fn enqueue_sora_2_pro_text_to_video_webhook<R: IntoUrl>(
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = args.duration
.map(|resolution| match resolution {
.map(|d| match d {
EnqueueSora2ProTextToVideoDurationSeconds::Four => 4,
EnqueueSora2ProTextToVideoDurationSeconds::Eight => 8,
EnqueueSora2ProTextToVideoDurationSeconds::Twelve => 12,
});
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueSora2ProTextToVideoResolution::Auto => "auto",
.map(|r| match r {
EnqueueSora2ProTextToVideoResolution::SevenTwentyP => "720p",
EnqueueSora2ProTextToVideoResolution::TenEightyP => "1080p",
})
.map(|resolution| resolution.to_string());
.map(|r| r.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueSora2ProTextToVideoAspectRatio::Auto => "auto",
.map(|ar| match ar {
EnqueueSora2ProTextToVideoAspectRatio::NineBySixteen => "9:16",
EnqueueSora2ProTextToVideoAspectRatio::SixteenByNine => "16:9",
})
.map(|resolution| resolution.to_string());
.map(|ar| ar.to_string());
let request = Sora2ProTextToVideoInput {
prompt: args.prompt,
// Optionals
duration,
resolution,
aspect_ratio,
// Constants
delete_video: Some(false),
};
@@ -116,17 +102,16 @@ pub async fn enqueue_sora_2_pro_text_to_video_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use super::*;
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::text::enqueue_sora_2_pro_text_to_video_webhook::{enqueue_sora_2_pro_text_to_video_webhook, EnqueueSora2ProTextToVideoArgs, EnqueueSora2ProTextToVideoAspectRatio, EnqueueSora2ProTextToVideoDurationSeconds, EnqueueSora2ProTextToVideoResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use strum::IntoEnumIterator;
#[tokio::test]
#[ignore]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueSora2ProTextToVideoArgs {
@@ -139,6 +124,76 @@ mod tests {
};
let result = enqueue_sora_2_pro_text_to_video_webhook(args).await?;
println!("result: {:?}", result);
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);
for ar in EnqueueSora2ProTextToVideoAspectRatio::iter() {
println!("--- aspect ratio: {:?} ---", ar);
let args = EnqueueSora2ProTextToVideoArgs {
prompt: "a serene mountain lake at sunset with gentle ripples".to_string(),
duration: Some(EnqueueSora2ProTextToVideoDurationSeconds::Four),
aspect_ratio: Some(ar),
resolution: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_pro_text_to_video_webhook(args).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);
for dur in EnqueueSora2ProTextToVideoDurationSeconds::iter() {
println!("--- duration: {:?} ---", dur);
let args = EnqueueSora2ProTextToVideoArgs {
prompt: "Suspenseful Hollywood movie. Interior, night. Customers are eating at a city diner. Suddenly a t-rex bursts its head through the large windows. The customers run.".to_string(),
duration: Some(dur),
aspect_ratio: Some(EnqueueSora2ProTextToVideoAspectRatio::SixteenByNine),
resolution: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_pro_text_to_video_webhook(args).await?;
println!("result: {:?}", result);
}
Ok(())
}
#[tokio::test]
#[ignore] // manually run — fires a real API request per variant (expensive)
async fn test_all_resolutions() -> AnyhowResult<()> {
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
for res in EnqueueSora2ProTextToVideoResolution::iter() {
println!("--- resolution: {:?} ---", res);
let args = EnqueueSora2ProTextToVideoArgs {
prompt: "a bird flying over an ocean at golden hour".to_string(),
duration: Some(EnqueueSora2ProTextToVideoDurationSeconds::Four),
aspect_ratio: Some(EnqueueSora2ProTextToVideoAspectRatio::SixteenByNine),
resolution: Some(res),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_pro_text_to_video_webhook(args).await?;
println!("result: {:?}", result);
}
Ok(())
}
@@ -1,8 +1,8 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::http::video::http_sora_2_text_to_video::{sora_2_text_to_video, Sora2TextToVideoInput};
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::sora::sora2::sora_2_text_to_video::{sora_2_text_to_video, Sora2TextToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -20,22 +20,20 @@ pub struct EnqueueSora2TextToVideoArgs<'a, R: IntoUrl> {
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, strum::EnumIter)]
pub enum EnqueueSora2TextToVideoDurationSeconds {
Four,
Eight,
Twelve,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, strum::EnumIter)]
pub enum EnqueueSora2TextToVideoResolution {
Auto,
SevenTwentyP,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, strum::EnumIter)]
pub enum EnqueueSora2TextToVideoAspectRatio {
Auto,
NineBySixteen,
SixteenByNine,
}
@@ -46,8 +44,8 @@ impl <U: IntoUrl> FalRequestCostCalculator for EnqueueSora2TextToVideoArgs<'_, U
// "The pricing is $0.1/s for Sora 2."
let duration = self.duration.unwrap_or(EnqueueSora2TextToVideoDurationSeconds::Four);
match duration {
EnqueueSora2TextToVideoDurationSeconds::Four => 40, // $0.10 * 4
EnqueueSora2TextToVideoDurationSeconds::Eight => 80, // $0.10 * 8
EnqueueSora2TextToVideoDurationSeconds::Four => 40, // $0.10 * 4
EnqueueSora2TextToVideoDurationSeconds::Eight => 80, // $0.10 * 8
EnqueueSora2TextToVideoDurationSeconds::Twelve => 120 // $0.10 * 12
}
}
@@ -60,34 +58,30 @@ pub async fn enqueue_sora_2_text_to_video_webhook<R: IntoUrl>(
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = args.duration
.map(|resolution| match resolution {
.map(|d| match d {
EnqueueSora2TextToVideoDurationSeconds::Four => 4,
EnqueueSora2TextToVideoDurationSeconds::Eight => 8,
EnqueueSora2TextToVideoDurationSeconds::Twelve => 12,
});
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueSora2TextToVideoResolution::Auto => "auto",
.map(|r| match r {
EnqueueSora2TextToVideoResolution::SevenTwentyP => "720p",
})
.map(|resolution| resolution.to_string());
.map(|r| r.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueSora2TextToVideoAspectRatio::Auto => "auto",
.map(|ar| match ar {
EnqueueSora2TextToVideoAspectRatio::NineBySixteen => "9:16",
EnqueueSora2TextToVideoAspectRatio::SixteenByNine => "16:9",
})
.map(|resolution| resolution.to_string());
.map(|ar| ar.to_string());
let request = Sora2TextToVideoInput {
prompt: args.prompt,
// Optionals
duration,
resolution,
aspect_ratio,
// Constants
delete_video: Some(false),
};
@@ -101,17 +95,16 @@ pub async fn enqueue_sora_2_text_to_video_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use super::*;
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::text::enqueue_sora_2_text_to_video_webhook::{enqueue_sora_2_text_to_video_webhook, EnqueueSora2TextToVideoArgs, EnqueueSora2TextToVideoAspectRatio, EnqueueSora2TextToVideoDurationSeconds, EnqueueSora2TextToVideoResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use strum::IntoEnumIterator;
#[tokio::test]
#[ignore]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueSora2TextToVideoArgs {
@@ -124,6 +117,53 @@ mod tests {
};
let result = enqueue_sora_2_text_to_video_webhook(args).await?;
println!("result: {:?}", result);
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);
for ar in EnqueueSora2TextToVideoAspectRatio::iter() {
println!("--- aspect ratio: {:?} ---", ar);
let args = EnqueueSora2TextToVideoArgs {
prompt: "a snowy mountain cabin at sunrise".to_string(),
duration: Some(EnqueueSora2TextToVideoDurationSeconds::Four),
aspect_ratio: Some(ar),
resolution: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_text_to_video_webhook(args).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);
for dur in EnqueueSora2TextToVideoDurationSeconds::iter() {
println!("--- duration: {:?} ---", dur);
let args = EnqueueSora2TextToVideoArgs {
prompt: "a corgi walks in front of a giant windmill in a beautiful village at sunrise".to_string(),
duration: Some(dur),
aspect_ratio: Some(EnqueueSora2TextToVideoAspectRatio::SixteenByNine),
resolution: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_text_to_video_webhook(args).await?;
println!("result: {:?}", result);
}
Ok(())
}
@@ -210,14 +210,14 @@ pub async fn sora_2_multi_function_video_gen_handler(
};
let aspect_ratio = match request.aspect_ratio {
Some(Sora2MultiFunctionVideoGenAspectRatio::Auto) => EnqueueSora2TextToVideoAspectRatio::Auto,
Some(Sora2MultiFunctionVideoGenAspectRatio::Auto) => EnqueueSora2TextToVideoAspectRatio::SixteenByNine,
Some(Sora2MultiFunctionVideoGenAspectRatio::SixteenByNine) => EnqueueSora2TextToVideoAspectRatio::SixteenByNine,
Some(Sora2MultiFunctionVideoGenAspectRatio::NineBySixteen) => EnqueueSora2TextToVideoAspectRatio::NineBySixteen,
None => EnqueueSora2TextToVideoAspectRatio::SixteenByNine,
};
let resolution = match request.resolution {
Some(Sora2MultiFunctionVideoGenResolution::Auto) => EnqueueSora2TextToVideoResolution::Auto,
Some(Sora2MultiFunctionVideoGenResolution::Auto) => EnqueueSora2TextToVideoResolution::SevenTwentyP,
Some(Sora2MultiFunctionVideoGenResolution::SevenTwentyP) => EnqueueSora2TextToVideoResolution::SevenTwentyP,
None => EnqueueSora2TextToVideoResolution::SevenTwentyP,
};
@@ -211,14 +211,14 @@ pub async fn sora_2_pro_multi_function_video_gen_handler(
};
let aspect_ratio = match request.aspect_ratio {
Some(Sora2ProMultiFunctionVideoGenAspectRatio::Auto) => EnqueueSora2ProTextToVideoAspectRatio::Auto,
Some(Sora2ProMultiFunctionVideoGenAspectRatio::Auto) => EnqueueSora2ProTextToVideoAspectRatio::SixteenByNine,
Some(Sora2ProMultiFunctionVideoGenAspectRatio::SixteenByNine) => EnqueueSora2ProTextToVideoAspectRatio::SixteenByNine,
Some(Sora2ProMultiFunctionVideoGenAspectRatio::NineBySixteen) => EnqueueSora2ProTextToVideoAspectRatio::NineBySixteen,
None => EnqueueSora2ProTextToVideoAspectRatio::SixteenByNine,
};
let resolution = match request.resolution {
Some(Sora2ProMultiFunctionVideoGenResolution::Auto) => EnqueueSora2ProTextToVideoResolution::Auto,
Some(Sora2ProMultiFunctionVideoGenResolution::Auto) => EnqueueSora2ProTextToVideoResolution::TenEightyP,
Some(Sora2ProMultiFunctionVideoGenResolution::SevenTwentyP) => EnqueueSora2ProTextToVideoResolution::SevenTwentyP,
Some(Sora2ProMultiFunctionVideoGenResolution::TenEightyP) => EnqueueSora2ProTextToVideoResolution::TenEightyP,
None => EnqueueSora2ProTextToVideoResolution::TenEightyP,
-14
View File
@@ -1944,20 +1944,6 @@ pub mod sdxl_controlnet_union;
)))
)]
pub mod skyreels_i2v;
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_sora"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_sora"
)))
)]
pub mod sora;
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
-1
View File
@@ -1 +0,0 @@
pub mod sora2;
@@ -1,4 +0,0 @@
pub mod sora_2_image_to_video;
pub mod sora_2_pro_image_to_video;
pub mod sora_2_pro_text_to_video;
pub mod sora_2_text_to_video;