diff --git a/Cargo.lock b/Cargo.lock index ae7e174eed..6d4ae087ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4185,6 +4185,7 @@ dependencies = [ "log", "reqwest 0.12.23", "serde", + "strum 0.28.0", "test_data", "testing", "tokio", diff --git a/crates/api_clients/fal_client/Cargo.toml b/crates/api_clients/fal_client/Cargo.toml index 7b022e5d41..c5520a9125 100644 --- a/crates/api_clients/fal_client/Cargo.toml +++ b/crates/api_clients/fal_client/Cargo.toml @@ -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 diff --git a/crates/api_clients/fal_client/src/requests/http/mod.rs b/crates/api_clients/fal_client/src/requests/http/mod.rs index 14995d4228..c10a6ca15e 100644 --- a/crates/api_clients/fal_client/src/requests/http/mod.rs +++ b/crates/api_clients/fal_client/src/requests/http/mod.rs @@ -1 +1,2 @@ pub mod image; +pub mod video; diff --git a/crates/api_clients/fal_client/src/requests/http/video/http_sora_2_image_to_video.rs b/crates/api_clients/fal_client/src/requests/http/video/http_sora_2_image_to_video.rs new file mode 100644 index 0000000000..8e51ee2350 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/http/video/http_sora_2_image_to_video.rs @@ -0,0 +1,47 @@ +use fal::request::FalRequest; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Sora2ImageToVideoInput { + pub prompt: String, + + /// Starting frame image URL. + pub image_url: String, + + /// Possible enum values: auto, 720p + /// Default value: "auto" + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// Possible enum values: auto, 9:16, 16:9 + /// Default value: "auto" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Duration in seconds. + /// Possible enum values: 4, 8, 12 + /// Default value: 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Whether to delete the video after generation for privacy reasons. + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub delete_video: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Sora2ImageToVideoVideoFile { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Sora2ImageToVideoOutput { + pub video: Sora2ImageToVideoVideoFile, +} + +pub fn sora_2_image_to_video( + params: Sora2ImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/sora-2/image-to-video", params) +} diff --git a/crates/api_clients/fal_client/src/requests/http/video/http_sora_2_pro_image_to_video.rs b/crates/api_clients/fal_client/src/requests/http/video/http_sora_2_pro_image_to_video.rs new file mode 100644 index 0000000000..53237d18ab --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/http/video/http_sora_2_pro_image_to_video.rs @@ -0,0 +1,47 @@ +use fal::request::FalRequest; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Sora2ProImageToVideoInput { + pub prompt: String, + + /// Starting frame image URL. + pub image_url: String, + + /// Possible enum values: auto, 720p, 1080p + /// Default value: "auto" + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// Possible enum values: auto, 9:16, 16:9 + /// Default value: "auto" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Duration in seconds. + /// Possible enum values: 4, 8, 12 + /// Default value: 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Whether to delete the video after generation for privacy reasons. + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub delete_video: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Sora2ProImageToVideoVideoFile { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Sora2ProImageToVideoOutput { + pub video: Sora2ProImageToVideoVideoFile, +} + +pub fn sora_2_pro_image_to_video( + params: Sora2ProImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/sora-2/image-to-video/pro", params) +} diff --git a/crates/api_clients/fal_client/src/requests/http/video/http_sora_2_pro_text_to_video.rs b/crates/api_clients/fal_client/src/requests/http/video/http_sora_2_pro_text_to_video.rs new file mode 100644 index 0000000000..16307c2596 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/http/video/http_sora_2_pro_text_to_video.rs @@ -0,0 +1,44 @@ +use fal::request::FalRequest; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Sora2ProTextToVideoInput { + pub prompt: String, + + /// Possible enum values: 720p, 1080p + /// Default value: "1080p" + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// Possible enum values: 9:16, 16:9 + /// Default value: "16:9" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Duration in seconds. + /// Possible enum values: 4, 8, 12 + /// Default value: 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Whether to delete the video after generation for privacy reasons. + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub delete_video: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Sora2ProTextToVideoVideoFile { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Sora2ProTextToVideoOutput { + pub video: Sora2ProTextToVideoVideoFile, +} + +pub fn sora_2_pro_text_to_video( + params: Sora2ProTextToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/sora-2/text-to-video/pro", params) +} diff --git a/crates/api_clients/fal_client/src/requests/http/video/http_sora_2_text_to_video.rs b/crates/api_clients/fal_client/src/requests/http/video/http_sora_2_text_to_video.rs new file mode 100644 index 0000000000..3c714229f4 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/http/video/http_sora_2_text_to_video.rs @@ -0,0 +1,44 @@ +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" + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// Possible enum values: 9:16, 16:9 + /// Default value: "16:9" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Duration in seconds. + /// Possible enum values: 4, 8, 12 + /// Default value: 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Whether to delete the video after generation for privacy reasons. + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub delete_video: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Sora2TextToVideoVideoFile { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Sora2TextToVideoOutput { + pub video: Sora2TextToVideoVideoFile, +} + +pub fn sora_2_text_to_video( + params: Sora2TextToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/sora-2/text-to-video", params) +} diff --git a/crates/api_clients/fal_client/src/requests/http/video/mod.rs b/crates/api_clients/fal_client/src/requests/http/video/mod.rs new file mode 100644 index 0000000000..898b70962f --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/http/video/mod.rs @@ -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; diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_image_to_video_webhook.rs index c9101f8720..06f24b0cc6 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_image_to_video_webhook.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_image_to_video_webhook.rs @@ -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, - pub resolution: Option, pub aspect_ratio: Option, @@ -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 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( ) -> Result { 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( #[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(()) } diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_pro_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_pro_image_to_video_webhook.rs index e3e190fa47..6fc0f25a24 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_pro_image_to_video_webhook.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_pro_image_to_video_webhook.rs @@ -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 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( @@ -74,36 +69,34 @@ pub async fn enqueue_sora_2_pro_image_to_video_webhook( ) -> Result { 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( #[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(()) } diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_pro_text_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_pro_text_to_video_webhook.rs index 278142aa6e..7523d00cd3 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_pro_text_to_video_webhook.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_pro_text_to_video_webhook.rs @@ -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 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( @@ -74,35 +64,31 @@ pub async fn enqueue_sora_2_pro_text_to_video_webhook( ) -> Result { 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( #[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: "a cat walking across a sunny windowsill".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(()) } diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_text_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_text_to_video_webhook.rs index 8a1280e90b..cdc69f9032 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_text_to_video_webhook.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_text_to_video_webhook.rs @@ -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 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( ) -> Result { 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( #[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 cat walking across a sunny windowsill".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(()) }