diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/generate_veo_2_image_to_video.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/generate_veo_2_image_to_video.rs index 3f221493ff..319a48e834 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/generate_veo_2_image_to_video.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/generate_veo_2_image_to_video.rs @@ -16,7 +16,8 @@ pub struct GenerateVeo2ImageToVideoRequest { /// Optional text prompt. pub prompt: Option, - + + /// **NB(bt): ASPECT RATIO IS ACTUALLY NOT USED IN VEO 2 IMAGE-TO-VIDEO** /// Optional: aspect ratio of the generated video. pub aspect_ratio: Option, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_2.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_2.rs index 1379c51549..0360b9d39c 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_2.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_2.rs @@ -8,7 +8,7 @@ use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_2::{ FalVeo2Mode, PlanFalVeo2, }; use fal_client::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{ - enqueue_veo_2_image_to_video_webhook, Veo2Args, + enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2AspectRatio, }; use fal_client::requests::webhook::video::text::enqueue_veo_2_text_to_video_webhook::{ enqueue_veo_2_text_to_video_webhook, Veo2TextToVideoArgs, @@ -25,7 +25,7 @@ pub async fn execute_fal_veo_2( negative_prompt: plan.negative_prompt.as_deref(), api_key: &fal_client.api_key, duration: plan.duration, - aspect_ratio: plan.aspect_ratio, + aspect_ratio: plan.aspect_ratio.unwrap_or(Veo2AspectRatio::Auto), webhook_url: fal_client.webhook_url.as_str(), }; enqueue_veo_2_text_to_video_webhook(args) @@ -33,12 +33,13 @@ pub async fn execute_fal_veo_2( .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))? } FalVeo2Mode::ImageToVideo { image_url } => { + // Image-to-video does not support aspect_ratio — the output + // inherits the source image's aspect ratio. let args = Veo2Args { image_url: image_url.as_str(), prompt: plan.prompt.as_str(), api_key: &fal_client.api_key, duration: plan.duration, - aspect_ratio: plan.aspect_ratio, webhook_url: fal_client.webhook_url.as_str(), }; enqueue_veo_2_image_to_video_webhook(args) diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/plan_generate_video_artcraft_veo_2.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/plan_generate_video_artcraft_veo_2.rs index a5089854df..f0cc9d675a 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/plan_generate_video_artcraft_veo_2.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/plan_generate_video_artcraft_veo_2.rs @@ -44,7 +44,14 @@ pub fn plan_generate_video_artcraft_veo_2<'a>( })); } - let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?; + // Aspect ratio only applies to text-to-video; image-to-video inherits + // the source frame's aspect ratio. + let is_image_mode = request.start_frame.is_some(); + let aspect_ratio = if is_image_mode { + None + } else { + plan_aspect_ratio(request.aspect_ratio, strategy)? + }; let duration = plan_duration(request.duration_seconds, strategy)?; Ok(VideoGenerationPlan::ArtcraftVeo2(PlanArtcraftVeo2 { diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_2.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_2.rs index cd3d0b8cd5..1223fbac87 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_2.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_2.rs @@ -22,7 +22,9 @@ pub struct PlanFalVeo2 { pub prompt: String, pub negative_prompt: Option, pub mode: FalVeo2Mode, - pub aspect_ratio: Veo2AspectRatio, + /// Only set for text-to-video. Image-to-video inherits the source frame's + /// aspect ratio and doesn't accept this parameter. + pub aspect_ratio: Option, pub duration: Veo2Duration, } @@ -45,7 +47,12 @@ pub fn plan_generate_video_fal_veo_2<'a>( None => FalVeo2Mode::TextToVideo, }; - let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?; + // Aspect ratio only applies to text-to-video; image-to-video inherits + // the source frame's aspect ratio. + let aspect_ratio = match &mode { + FalVeo2Mode::TextToVideo => Some(plan_aspect_ratio(request.aspect_ratio, strategy)?), + FalVeo2Mode::ImageToVideo { .. } => None, + }; let duration = plan_duration(request.duration_seconds, strategy)?; Ok(VideoGenerationPlan::FalVeo2(PlanFalVeo2 { diff --git a/crates/api_clients/fal_client/src/requests/http/video/image/http_veo_2_image_to_video.rs b/crates/api_clients/fal_client/src/requests/http/video/image/http_veo_2_image_to_video.rs index d945289354..a9f5e7357b 100644 --- a/crates/api_clients/fal_client/src/requests/http/video/image/http_veo_2_image_to_video.rs +++ b/crates/api_clients/fal_client/src/requests/http/video/image/http_veo_2_image_to_video.rs @@ -7,10 +7,6 @@ pub struct Veo2ImageToVideoInput { pub prompt: String, - /// Options: "auto", "16:9", "9:16" - #[serde(skip_serializing_if = "Option::is_none")] - pub aspect_ratio: Option, - /// Options: "4s", "6s", "8s" #[serde(skip_serializing_if = "Option::is_none")] pub duration: Option, diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_2_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_2_image_to_video_webhook.rs index e9b58d770b..c40b810d9f 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_2_image_to_video_webhook.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_2_image_to_video_webhook.rs @@ -6,16 +6,17 @@ use crate::requests::http::video::image::http_veo_2_image_to_video::{veo_2_image use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; use reqwest::IntoUrl; +/// Args for Veo 2 image-to-video. Note: image-to-video does NOT support +/// aspect_ratio — the output inherits the source image's aspect ratio. pub struct Veo2Args<'a, U: IntoUrl, V: IntoUrl> { pub image_url: U, pub webhook_url: V, pub prompt: &'a str, pub api_key: &'a FalApiKey, pub duration: Veo2Duration, - pub aspect_ratio: Veo2AspectRatio, } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Veo2Duration { Default, // NB: Default is 5 seconds FiveSeconds, @@ -24,6 +25,9 @@ pub enum Veo2Duration { EightSeconds, } +/// Aspect ratio enum — used by the text-to-video endpoint only. +/// Kept here because it's the canonical location that the text-to-video +/// webhook and the router plan both re-export from. #[derive(Copy, Clone, Debug)] pub enum Veo2AspectRatio { Auto, @@ -59,22 +63,13 @@ pub async fn enqueue_veo_2_image_to_video_webhook( Veo2Duration::SevenSeconds => Some("7s".to_string()), Veo2Duration::EightSeconds => Some("8s".to_string()), }; - - let aspect_ratio = match args.aspect_ratio { - Veo2AspectRatio::Auto => Some("auto".to_string()), - Veo2AspectRatio::AutoPreferPortrait => Some("auto_prefer_portrait".to_string()), - Veo2AspectRatio::WideSixteenNine => Some("16:9".to_string()), - Veo2AspectRatio::TallNineSixteen => Some("9:16".to_string()), - }; let image_url = args.image_url.as_str().to_string(); let request = Veo2ImageToVideoInput { image_url, prompt: args.prompt.to_string(), - aspect_ratio, duration, - // Maybe expose these later }; let result = veo_2_image_to_video(request) @@ -89,14 +84,14 @@ pub async fn enqueue_veo_2_image_to_video_webhook( #[cfg(test)] mod tests { use crate::creds::fal_api_key::FalApiKey; - use crate::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2AspectRatio, Veo2Duration}; + use crate::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2Duration}; use errors::AnyhowResult; use std::fs::read_to_string; use test_data::web::image_urls::MOUNTAIN_TREE_IMAGE_URL; #[tokio::test] #[ignore] - async fn test_veo_2_video_wide() -> AnyhowResult<()> { + async fn test_veo_2_image_to_video() -> AnyhowResult<()> { // XXX: Don't commit secrets! let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; @@ -107,29 +102,6 @@ mod tests { prompt: "a shot of the mountains as the sun sets and reveals the moon and stars", api_key: &api_key, duration: Veo2Duration::Default, - aspect_ratio: Veo2AspectRatio::WideSixteenNine, - webhook_url: "https://example.com/webhook", - }; - - let result = enqueue_veo_2_image_to_video_webhook(args).await?; - - Ok(()) - } - - #[tokio::test] - #[ignore] - async fn test_veo_2_video_tall() -> AnyhowResult<()> { - // XXX: Don't commit secrets! - let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; - - let api_key = FalApiKey::from_str(&secret); - - let args = Veo2Args { - image_url: MOUNTAIN_TREE_IMAGE_URL, - prompt: "a shot of the mountains as the sun sets and reveals the moon and stars", - api_key: &api_key, - duration: Veo2Duration::Default, - aspect_ratio: Veo2AspectRatio::TallNineSixteen, webhook_url: "https://example.com/webhook", }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/image/generate_veo_2_image_to_video_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/image/generate_veo_2_image_to_video_handler.rs index ed9f49d3b4..55e262fa8b 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/image/generate_veo_2_image_to_video_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/image/generate_veo_2_image_to_video_handler.rs @@ -148,13 +148,9 @@ pub async fn generate_veo_2_image_to_video_handler( .map(|prompt| prompt.trim()) .unwrap_or_else(|| ""); - let aspect_ratio = match &request.aspect_ratio { - Some(GenerateVeo2AspectRatio::Auto) => Veo2AspectRatio::Auto, - Some(GenerateVeo2AspectRatio::AutoPreferPortrait) => Veo2AspectRatio::AutoPreferPortrait, - Some(GenerateVeo2AspectRatio::WideSixteenNine) => Veo2AspectRatio::WideSixteenNine, - Some(GenerateVeo2AspectRatio::TallNineSixteen) => Veo2AspectRatio::TallNineSixteen, - None => Veo2AspectRatio::WideSixteenNine, // Default to 16:9 - }; + if let Some(aspect_ratio) = &request.aspect_ratio { + warn!("Aspect ratio is not read in Veo 2 image-to-video"); + } let duration = match &request.duration { Some(GenerateVeo2Duration::FiveSeconds) => Veo2Duration::FiveSeconds, @@ -169,7 +165,6 @@ pub async fn generate_veo_2_image_to_video_handler( webhook_url: &server_state.fal.webhook_url, duration, prompt, - aspect_ratio, api_key: &server_state.fal.api_key, }; @@ -222,12 +217,7 @@ pub async fn generate_veo_2_image_to_video_handler( maybe_negative_prompt: None, maybe_other_args: None, maybe_generation_mode: Some(CommonGenerationMode::Keyframe), // TODO: This endpoint only supports keyframes for now - maybe_aspect_ratio: request.aspect_ratio.as_ref().map(|ar| match ar { - GenerateVeo2AspectRatio::Auto => CommonAspectRatio::Auto, - GenerateVeo2AspectRatio::AutoPreferPortrait => CommonAspectRatio::Auto, - GenerateVeo2AspectRatio::WideSixteenNine => CommonAspectRatio::WideSixteenByNine, - GenerateVeo2AspectRatio::TallNineSixteen => CommonAspectRatio::TallNineBySixteen, - }), + maybe_aspect_ratio: None, // NB: The aspect ratio is not read for Veo 2 image-to-video maybe_resolution: None, maybe_batch_count: None, maybe_generate_audio: None, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/test_veo_2.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/test_veo_2.rs index 5a0cc91af7..17fdbd86e4 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/test_veo_2.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/test_veo_2.rs @@ -244,7 +244,7 @@ mod tests { #[test] fn default_aspect_ratio_is_auto() { with_text_plan(&make_request(Some("p"), None, None, None), |plan| { - assert!(matches!(plan.aspect_ratio, Veo2AspectRatio::Auto)); + assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::Auto))); }); } @@ -252,7 +252,7 @@ mod tests { fn auto_yields_auto() { for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] { with_text_plan(&make_request(Some("p"), Some(ar), None, None), |plan| { - assert!(matches!(plan.aspect_ratio, Veo2AspectRatio::Auto)); + assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::Auto))); }); } } @@ -261,7 +261,7 @@ mod tests { fn wide_16x9_yields_wide() { for ar in [CommonAspectRatio::WideSixteenByNine, CommonAspectRatio::Wide] { with_text_plan(&make_request(Some("p"), Some(ar), None, None), |plan| { - assert!(matches!(plan.aspect_ratio, Veo2AspectRatio::WideSixteenNine)); + assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::WideSixteenNine))); }); } } @@ -270,7 +270,7 @@ mod tests { fn tall_9x16_yields_tall() { for ar in [CommonAspectRatio::TallNineBySixteen, CommonAspectRatio::Tall] { with_text_plan(&make_request(Some("p"), Some(ar), None, None), |plan| { - assert!(matches!(plan.aspect_ratio, Veo2AspectRatio::TallNineSixteen)); + assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::TallNineSixteen))); }); } } @@ -282,7 +282,7 @@ mod tests { CommonAspectRatio::WideFourByThree, CommonAspectRatio::TallThreeByFour, ] { with_text_plan(&make_request(Some("p"), Some(ar), None, None), |plan| { - assert!(matches!(plan.aspect_ratio, Veo2AspectRatio::Auto)); + assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::Auto))); }); } } @@ -380,14 +380,23 @@ mod tests { }); } - // ── Aspect ratio in image mode ──────────────────────────────────────── + // ── Aspect ratio is None in image mode (i2v inherits source AR) ──────── #[test] - fn aspect_ratio_passes_through_in_image_mode() { + fn aspect_ratio_is_none_in_image_mode() { let (start, map) = fake_token("mf_start0000000000000000000000"); let request = make_request(Some("p"), Some(CommonAspectRatio::WideSixteenByNine), None, Some(start)); with_image_plan(&request, &map, |plan| { - assert!(matches!(plan.aspect_ratio, Veo2AspectRatio::WideSixteenNine)); + assert!(plan.aspect_ratio.is_none(), "image-to-video should not have aspect_ratio"); + }); + } + + #[test] + fn aspect_ratio_is_none_in_image_mode_even_with_tall() { + let (start, map) = fake_token("mf_start0000000000000000000000"); + let request = make_request(Some("p"), Some(CommonAspectRatio::TallNineBySixteen), None, Some(start)); + with_image_plan(&request, &map, |plan| { + assert!(plan.aspect_ratio.is_none()); }); }