diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_2.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_2.rs new file mode 100644 index 0000000000..1a5e28d878 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_2.rs @@ -0,0 +1,20 @@ +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_2::PlanFalVeo2; +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; + +pub(crate) fn estimate_video_cost_fal_veo_2( + plan: &PlanFalVeo2, +) -> VideoGenerationCostEstimate { + // 5s = $2.50, +$0.50 per additional second. + let seconds = plan.duration_seconds_for_cost(); + let extra = seconds.saturating_sub(5); + let cost_in_usd_cents = 250 + extra * 50; + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_3.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_3.rs new file mode 100644 index 0000000000..190ac3ab46 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_3.rs @@ -0,0 +1,20 @@ +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3::PlanFalVeo3; +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; + +pub(crate) fn estimate_video_cost_fal_veo_3( + plan: &PlanFalVeo3, +) -> VideoGenerationCostEstimate { + // $0.20/sec audio off, $0.40/sec audio on (720p/1080p). + let seconds = plan.duration_seconds_for_cost(); + let per_second_cents: u64 = if plan.generate_audio { 40 } else { 20 }; + let cost_in_usd_cents = per_second_cents * seconds; + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_3_fast.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_3_fast.rs new file mode 100644 index 0000000000..d50a62757b --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_3_fast.rs @@ -0,0 +1,20 @@ +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3_fast::PlanFalVeo3Fast; +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; + +pub(crate) fn estimate_video_cost_fal_veo_3_fast( + plan: &PlanFalVeo3Fast, +) -> VideoGenerationCostEstimate { + // $0.10/sec audio off, $0.15/sec audio on (720p/1080p). + let seconds = plan.duration_seconds_for_cost(); + let per_second_cents: u64 = if plan.generate_audio { 15 } else { 10 }; + let cost_in_usd_cents = per_second_cents * seconds; + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_3p1.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_3p1.rs new file mode 100644 index 0000000000..93af66175d --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_3p1.rs @@ -0,0 +1,20 @@ +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1::PlanFalVeo3p1; +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; + +pub(crate) fn estimate_video_cost_fal_veo_3p1( + plan: &PlanFalVeo3p1, +) -> VideoGenerationCostEstimate { + // $0.20/sec audio off, $0.40/sec audio on (720p/1080p). + let seconds = plan.duration_seconds_for_cost(); + let per_second_cents: u64 = if plan.generate_audio_for_cost() { 40 } else { 20 }; + let cost_in_usd_cents = per_second_cents * seconds; + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_3p1_fast.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_3p1_fast.rs new file mode 100644 index 0000000000..f4088f65ac --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/estimate_video_cost_fal_veo_3p1_fast.rs @@ -0,0 +1,20 @@ +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1_fast::PlanFalVeo3p1Fast; +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; + +pub(crate) fn estimate_video_cost_fal_veo_3p1_fast( + plan: &PlanFalVeo3p1Fast, +) -> VideoGenerationCostEstimate { + // $0.10/sec audio off, $0.15/sec audio on (720p/1080p). + let seconds = plan.inner.duration_seconds_for_cost(); + let per_second_cents: u64 = if plan.inner.generate_audio_for_cost() { 15 } else { 10 }; + let cost_in_usd_cents = per_second_cents * seconds; + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/mod.rs new file mode 100644 index 0000000000..bd34d80da2 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/fal/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod estimate_video_cost_fal_veo_2; +pub(crate) mod estimate_video_cost_fal_veo_3; +pub(crate) mod estimate_video_cost_fal_veo_3_fast; +pub(crate) mod estimate_video_cost_fal_veo_3p1; +pub(crate) mod estimate_video_cost_fal_veo_3p1_fast; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/mod.rs index 079266e6ee..950a44c484 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/mod.rs @@ -1,3 +1,4 @@ pub (crate) mod artcraft; +pub (crate) mod fal; pub (crate) mod muapi; pub (crate) mod seedance2pro; 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 new file mode 100644 index 0000000000..b3828e30dd --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_2.rs @@ -0,0 +1,33 @@ +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_2::PlanFalVeo2; +use fal_client::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{ + enqueue_veo_2_image_to_video_webhook, Veo2Args, +}; + +pub async fn execute_fal_veo_2( + plan: &PlanFalVeo2, + fal_client: &RouterFalClient, +) -> Result { + let args = Veo2Args { + image_url: plan.start_frame_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(), + }; + + let webhook_response = enqueue_veo_2_image_to_video_webhook(args) + .await + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + })) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_3.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_3.rs new file mode 100644 index 0000000000..1ba33a0603 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_3.rs @@ -0,0 +1,35 @@ +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3::PlanFalVeo3; +use fal_client::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{ + enqueue_veo_3_image_to_video_webhook, Veo3Args, +}; + +pub async fn execute_fal_veo_3( + plan: &PlanFalVeo3, + fal_client: &RouterFalClient, +) -> Result { + let args = Veo3Args { + image_url: plan.start_frame_url.as_str(), + prompt: plan.prompt.as_str(), + duration: plan.duration, + aspect_ratio: plan.aspect_ratio, + resolution: plan.resolution, + generate_audio: plan.generate_audio, + api_key: &fal_client.api_key, + webhook_url: fal_client.webhook_url.as_str(), + }; + + let webhook_response = enqueue_veo_3_image_to_video_webhook(args) + .await + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + })) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_3_fast.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_3_fast.rs new file mode 100644 index 0000000000..af3b605fef --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_3_fast.rs @@ -0,0 +1,34 @@ +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3_fast::PlanFalVeo3Fast; +use fal_client::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{ + enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs, +}; + +pub async fn execute_fal_veo_3_fast( + plan: &PlanFalVeo3Fast, + fal_client: &RouterFalClient, +) -> Result { + let args = Veo3FastArgs { + prompt: plan.prompt.as_str(), + image_url: plan.start_frame_url.as_str(), + duration: plan.duration, + api_key: &fal_client.api_key, + resolution: plan.resolution, + generate_audio: plan.generate_audio, + webhook_url: fal_client.webhook_url.as_str(), + }; + + let webhook_response = enqueue_veo_3_fast_image_to_video_webhook(args) + .await + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + })) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_3p1.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_3p1.rs new file mode 100644 index 0000000000..90dba08a4a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_3p1.rs @@ -0,0 +1,154 @@ +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1::{ + FalVeo3p1AspectRatio, FalVeo3p1Duration, FalVeo3p1Mode, FalVeo3p1Resolution, PlanFalVeo3p1, +}; +use fal_client::requests::webhook::video::image::enqueue_veo_3p1_first_last_frame_image_to_video_webhook::{ + enqueue_veo_3p1_first_last_frame_image_to_video_webhook, + EnqueueVeo3p1FirstLastFrameImageToVideoArgs, + EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio, + EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds, + EnqueueVeo3p1FirstLastFrameImageToVideoResolution, +}; +use fal_client::requests::webhook::video::image::enqueue_veo_3p1_image_to_video_webhook::{ + enqueue_veo_3p1_image_to_video_webhook, EnqueueVeo3p1ImageToVideoArgs, + EnqueueVeo3p1ImageToVideoAspectRatio, EnqueueVeo3p1ImageToVideoDurationSeconds, + EnqueueVeo3p1ImageToVideoResolution, +}; +use fal_client::requests::webhook::video::text::enqueue_veo_3p1_text_to_video_webhook::{ + enqueue_veo_3p1_text_to_video_webhook, EnqueueVeo3p1TextToVideoArgs, + EnqueueVeo3p1TextToVideoAspectRatio, EnqueueVeo3p1TextToVideoDurationSeconds, + EnqueueVeo3p1TextToVideoResolution, +}; + +pub async fn execute_fal_veo_3p1( + plan: &PlanFalVeo3p1, + fal_client: &RouterFalClient, +) -> Result { + let webhook_response = match &plan.mode { + FalVeo3p1Mode::TextToVideo => { + let args = EnqueueVeo3p1TextToVideoArgs { + prompt: plan.prompt.clone(), + duration: plan.duration.map(to_t2v_duration), + aspect_ratio: plan.aspect_ratio.map(to_t2v_aspect_ratio), + resolution: plan.resolution.map(to_t2v_resolution), + generate_audio: plan.generate_audio, + enhance_prompt: None, + negative_prompt: plan.negative_prompt.clone(), + seed: None, + auto_fix: None, + webhook_url: fal_client.webhook_url.as_str(), + api_key: &fal_client.api_key, + }; + enqueue_veo_3p1_text_to_video_webhook(args).await + } + FalVeo3p1Mode::ImageToVideo { start_frame_url } => { + let args = EnqueueVeo3p1ImageToVideoArgs { + prompt: plan.prompt.clone(), + image_url: start_frame_url.clone(), + duration: plan.duration.map(to_i2v_duration), + aspect_ratio: plan.aspect_ratio.map(to_i2v_aspect_ratio), + resolution: plan.resolution.map(to_i2v_resolution), + generate_audio: plan.generate_audio, + webhook_url: fal_client.webhook_url.as_str(), + api_key: &fal_client.api_key, + }; + enqueue_veo_3p1_image_to_video_webhook(args).await + } + FalVeo3p1Mode::FirstLastFrame { first_frame_url, last_frame_url } => { + let args = EnqueueVeo3p1FirstLastFrameImageToVideoArgs { + prompt: plan.prompt.clone(), + first_frame_url: first_frame_url.clone(), + last_frame_url: last_frame_url.clone(), + duration: plan.duration.map(to_flf_duration), + aspect_ratio: plan.aspect_ratio.map(to_flf_aspect_ratio), + resolution: plan.resolution.map(to_flf_resolution), + generate_audio: plan.generate_audio, + webhook_url: fal_client.webhook_url.as_str(), + api_key: &fal_client.api_key, + }; + enqueue_veo_3p1_first_last_frame_image_to_video_webhook(args).await + } + }; + + let webhook_response = webhook_response + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + })) +} + +fn to_t2v_duration(d: FalVeo3p1Duration) -> EnqueueVeo3p1TextToVideoDurationSeconds { + match d { + FalVeo3p1Duration::Four => EnqueueVeo3p1TextToVideoDurationSeconds::Four, + FalVeo3p1Duration::Six => EnqueueVeo3p1TextToVideoDurationSeconds::Six, + FalVeo3p1Duration::Eight => EnqueueVeo3p1TextToVideoDurationSeconds::Eight, + } +} + +fn to_t2v_aspect_ratio(a: FalVeo3p1AspectRatio) -> EnqueueVeo3p1TextToVideoAspectRatio { + match a { + FalVeo3p1AspectRatio::Auto => EnqueueVeo3p1TextToVideoAspectRatio::Auto, + FalVeo3p1AspectRatio::SixteenByNine => EnqueueVeo3p1TextToVideoAspectRatio::SixteenByNine, + FalVeo3p1AspectRatio::NineBySixteen => EnqueueVeo3p1TextToVideoAspectRatio::NineBySixteen, + } +} + +fn to_t2v_resolution(r: FalVeo3p1Resolution) -> EnqueueVeo3p1TextToVideoResolution { + match r { + FalVeo3p1Resolution::SevenTwentyP => EnqueueVeo3p1TextToVideoResolution::SevenTwentyP, + FalVeo3p1Resolution::TenEightyP => EnqueueVeo3p1TextToVideoResolution::TenEightyP, + } +} + +fn to_i2v_duration(d: FalVeo3p1Duration) -> EnqueueVeo3p1ImageToVideoDurationSeconds { + match d { + FalVeo3p1Duration::Four => EnqueueVeo3p1ImageToVideoDurationSeconds::Four, + FalVeo3p1Duration::Six => EnqueueVeo3p1ImageToVideoDurationSeconds::Six, + FalVeo3p1Duration::Eight => EnqueueVeo3p1ImageToVideoDurationSeconds::Eight, + } +} + +fn to_i2v_aspect_ratio(a: FalVeo3p1AspectRatio) -> EnqueueVeo3p1ImageToVideoAspectRatio { + match a { + FalVeo3p1AspectRatio::Auto => EnqueueVeo3p1ImageToVideoAspectRatio::Auto, + FalVeo3p1AspectRatio::SixteenByNine => EnqueueVeo3p1ImageToVideoAspectRatio::SixteenByNine, + FalVeo3p1AspectRatio::NineBySixteen => EnqueueVeo3p1ImageToVideoAspectRatio::NineBySixteen, + } +} + +fn to_i2v_resolution(r: FalVeo3p1Resolution) -> EnqueueVeo3p1ImageToVideoResolution { + match r { + FalVeo3p1Resolution::SevenTwentyP => EnqueueVeo3p1ImageToVideoResolution::SevenTwentyP, + FalVeo3p1Resolution::TenEightyP => EnqueueVeo3p1ImageToVideoResolution::TenEightyP, + } +} + +fn to_flf_duration(d: FalVeo3p1Duration) -> EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds { + match d { + FalVeo3p1Duration::Four => EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Four, + FalVeo3p1Duration::Six => EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Six, + FalVeo3p1Duration::Eight => EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Eight, + } +} + +fn to_flf_aspect_ratio(a: FalVeo3p1AspectRatio) -> EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio { + match a { + FalVeo3p1AspectRatio::Auto => EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::Auto, + FalVeo3p1AspectRatio::SixteenByNine => EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::SixteenByNine, + FalVeo3p1AspectRatio::NineBySixteen => EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::NineBySixteen, + } +} + +fn to_flf_resolution(r: FalVeo3p1Resolution) -> EnqueueVeo3p1FirstLastFrameImageToVideoResolution { + match r { + FalVeo3p1Resolution::SevenTwentyP => EnqueueVeo3p1FirstLastFrameImageToVideoResolution::SevenTwentyP, + FalVeo3p1Resolution::TenEightyP => EnqueueVeo3p1FirstLastFrameImageToVideoResolution::TenEightyP, + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_3p1_fast.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_3p1_fast.rs new file mode 100644 index 0000000000..31dba786d9 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/generate_video_fal_veo_3p1_fast.rs @@ -0,0 +1,157 @@ +use crate::client::router_fal_client::RouterFalClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + FalVideoResponsePayload, GenerateVideoResponse, +}; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1::{ + FalVeo3p1AspectRatio, FalVeo3p1Duration, FalVeo3p1Mode, FalVeo3p1Resolution, +}; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1_fast::PlanFalVeo3p1Fast; +use fal_client::requests::webhook::video::image::enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook::{ + enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook, + EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs, + EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio, + EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds, + EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution, +}; +use fal_client::requests::webhook::video::image::enqueue_veo_3p1_fast_image_to_video_webhook::{ + enqueue_veo_3p1_fast_image_to_video_webhook, EnqueueVeo3p1FastImageToVideoArgs, + EnqueueVeo3p1FastImageToVideoAspectRatio, EnqueueVeo3p1FastImageToVideoDurationSeconds, + EnqueueVeo3p1FastImageToVideoResolution, +}; +use fal_client::requests::webhook::video::text::enqueue_veo_3p1_fast_text_to_video_webhook::{ + enqueue_veo_3p1_fast_text_to_video_webhook, EnqueueVeo3p1FastTextToVideoArgs, + EnqueueVeo3p1FastTextToVideoAspectRatio, EnqueueVeo3p1FastTextToVideoDurationSeconds, + EnqueueVeo3p1FastTextToVideoResolution, +}; + +pub async fn execute_fal_veo_3p1_fast( + plan: &PlanFalVeo3p1Fast, + fal_client: &RouterFalClient, +) -> Result { + let inner = &plan.inner; + + let webhook_response = match &inner.mode { + FalVeo3p1Mode::TextToVideo => { + let args = EnqueueVeo3p1FastTextToVideoArgs { + prompt: inner.prompt.clone(), + duration: inner.duration.map(to_t2v_duration), + aspect_ratio: inner.aspect_ratio.map(to_t2v_aspect_ratio), + resolution: inner.resolution.map(to_t2v_resolution), + generate_audio: inner.generate_audio, + enhance_prompt: None, + negative_prompt: inner.negative_prompt.clone(), + seed: None, + auto_fix: None, + webhook_url: fal_client.webhook_url.as_str(), + api_key: &fal_client.api_key, + }; + enqueue_veo_3p1_fast_text_to_video_webhook(args).await + } + FalVeo3p1Mode::ImageToVideo { start_frame_url } => { + let args = EnqueueVeo3p1FastImageToVideoArgs { + prompt: inner.prompt.clone(), + image_url: start_frame_url.clone(), + duration: inner.duration.map(to_i2v_duration), + aspect_ratio: inner.aspect_ratio.map(to_i2v_aspect_ratio), + resolution: inner.resolution.map(to_i2v_resolution), + generate_audio: inner.generate_audio, + webhook_url: fal_client.webhook_url.as_str(), + api_key: &fal_client.api_key, + }; + enqueue_veo_3p1_fast_image_to_video_webhook(args).await + } + FalVeo3p1Mode::FirstLastFrame { first_frame_url, last_frame_url } => { + let args = EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs { + prompt: inner.prompt.clone(), + first_frame_url: first_frame_url.clone(), + last_frame_url: last_frame_url.clone(), + duration: inner.duration.map(to_flf_duration), + aspect_ratio: inner.aspect_ratio.map(to_flf_aspect_ratio), + resolution: inner.resolution.map(to_flf_resolution), + generate_audio: inner.generate_audio, + webhook_url: fal_client.webhook_url.as_str(), + api_key: &fal_client.api_key, + }; + enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook(args).await + } + }; + + let webhook_response = webhook_response + .map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?; + + Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload { + request_id: webhook_response.request_id, + gateway_request_id: webhook_response.gateway_request_id, + })) +} + +fn to_t2v_duration(d: FalVeo3p1Duration) -> EnqueueVeo3p1FastTextToVideoDurationSeconds { + match d { + FalVeo3p1Duration::Four => EnqueueVeo3p1FastTextToVideoDurationSeconds::Four, + FalVeo3p1Duration::Six => EnqueueVeo3p1FastTextToVideoDurationSeconds::Six, + FalVeo3p1Duration::Eight => EnqueueVeo3p1FastTextToVideoDurationSeconds::Eight, + } +} + +fn to_t2v_aspect_ratio(a: FalVeo3p1AspectRatio) -> EnqueueVeo3p1FastTextToVideoAspectRatio { + match a { + FalVeo3p1AspectRatio::Auto => EnqueueVeo3p1FastTextToVideoAspectRatio::Auto, + FalVeo3p1AspectRatio::SixteenByNine => EnqueueVeo3p1FastTextToVideoAspectRatio::SixteenByNine, + FalVeo3p1AspectRatio::NineBySixteen => EnqueueVeo3p1FastTextToVideoAspectRatio::NineBySixteen, + } +} + +fn to_t2v_resolution(r: FalVeo3p1Resolution) -> EnqueueVeo3p1FastTextToVideoResolution { + match r { + FalVeo3p1Resolution::SevenTwentyP => EnqueueVeo3p1FastTextToVideoResolution::SevenTwentyP, + FalVeo3p1Resolution::TenEightyP => EnqueueVeo3p1FastTextToVideoResolution::TenEightyP, + } +} + +fn to_i2v_duration(d: FalVeo3p1Duration) -> EnqueueVeo3p1FastImageToVideoDurationSeconds { + match d { + FalVeo3p1Duration::Four => EnqueueVeo3p1FastImageToVideoDurationSeconds::Four, + FalVeo3p1Duration::Six => EnqueueVeo3p1FastImageToVideoDurationSeconds::Six, + FalVeo3p1Duration::Eight => EnqueueVeo3p1FastImageToVideoDurationSeconds::Eight, + } +} + +fn to_i2v_aspect_ratio(a: FalVeo3p1AspectRatio) -> EnqueueVeo3p1FastImageToVideoAspectRatio { + match a { + FalVeo3p1AspectRatio::Auto => EnqueueVeo3p1FastImageToVideoAspectRatio::Auto, + FalVeo3p1AspectRatio::SixteenByNine => EnqueueVeo3p1FastImageToVideoAspectRatio::SixteenByNine, + FalVeo3p1AspectRatio::NineBySixteen => EnqueueVeo3p1FastImageToVideoAspectRatio::NineBySixteen, + } +} + +fn to_i2v_resolution(r: FalVeo3p1Resolution) -> EnqueueVeo3p1FastImageToVideoResolution { + match r { + FalVeo3p1Resolution::SevenTwentyP => EnqueueVeo3p1FastImageToVideoResolution::SevenTwentyP, + FalVeo3p1Resolution::TenEightyP => EnqueueVeo3p1FastImageToVideoResolution::TenEightyP, + } +} + +fn to_flf_duration(d: FalVeo3p1Duration) -> EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds { + match d { + FalVeo3p1Duration::Four => EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Four, + FalVeo3p1Duration::Six => EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Six, + FalVeo3p1Duration::Eight => EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Eight, + } +} + +fn to_flf_aspect_ratio(a: FalVeo3p1AspectRatio) -> EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio { + match a { + FalVeo3p1AspectRatio::Auto => EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::Auto, + FalVeo3p1AspectRatio::SixteenByNine => EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::SixteenByNine, + FalVeo3p1AspectRatio::NineBySixteen => EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::NineBySixteen, + } +} + +fn to_flf_resolution(r: FalVeo3p1Resolution) -> EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution { + match r { + FalVeo3p1Resolution::SevenTwentyP => EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution::SevenTwentyP, + FalVeo3p1Resolution::TenEightyP => EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution::TenEightyP, + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/mod.rs new file mode 100644 index 0000000000..745852a205 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/fal/mod.rs @@ -0,0 +1,5 @@ +pub mod generate_video_fal_veo_2; +pub mod generate_video_fal_veo_3; +pub mod generate_video_fal_veo_3_fast; +pub mod generate_video_fal_veo_3p1; +pub mod generate_video_fal_veo_3p1_fast; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/mod.rs index d1dee1d0a0..da07b9e7b8 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/mod.rs @@ -1,3 +1,4 @@ pub mod artcraft; +pub mod fal; pub mod muapi; pub mod seedance2pro; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request.rs index 938b531db8..e976f356ce 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request.rs @@ -13,6 +13,11 @@ use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraf use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::plan_generate_video_artcraft_kling3p0_standard; use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance1p5_pro::plan_generate_video_artcraft_seedance1p5_pro; use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::plan_generate_video_artcraft_seedance2p0; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_2::plan_generate_video_fal_veo_2; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3::plan_generate_video_fal_veo_3; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3_fast::plan_generate_video_fal_veo_3_fast; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1::plan_generate_video_fal_veo_3p1; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1_fast::plan_generate_video_fal_veo_3p1_fast; use crate::generate::generate_video::plan::muapi::plan_generate_video_muapi_seedance2p0::plan_generate_video_muapi_seedance2p0; use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::plan_generate_video_seedance2pro_seedance2p0; use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan; @@ -83,6 +88,7 @@ impl<'a> GenerateVideoRequest<'a> { pub fn build(&self) -> Result, ArtcraftRouterError> { match self.provider { Provider::Artcraft => self.build_artcraft(), + Provider::Fal => self.build_fal(), Provider::Muapi => self.build_muapi(), Provider::Seedance2Pro => self.build_seedance2pro(), _ => self.unsupported_provider(), @@ -99,6 +105,17 @@ impl<'a> GenerateVideoRequest<'a> { } } + fn build_fal(&self) -> Result, ArtcraftRouterError> { + match self.model { + CommonVideoModel::Veo2 => plan_generate_video_fal_veo_2(self), + CommonVideoModel::Veo3 => plan_generate_video_fal_veo_3(self), + CommonVideoModel::Veo3Fast => plan_generate_video_fal_veo_3_fast(self), + CommonVideoModel::Veo3p1 => plan_generate_video_fal_veo_3p1(self), + CommonVideoModel::Veo3p1Fast => plan_generate_video_fal_veo_3p1_fast(self), + _ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))), + } + } + fn build_muapi(&self) -> Result, ArtcraftRouterError> { match self.model { CommonVideoModel::Seedance2p0 => plan_generate_video_muapi_seedance2p0(self), diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_response.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_response.rs index 0c67befe8c..f71d5b7a68 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_response.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_response.rs @@ -18,11 +18,18 @@ pub struct Seedance2proVideoResponsePayload { pub task_id: String, } +#[derive(Clone, Debug)] +pub struct FalVideoResponsePayload { + pub request_id: Option, + pub gateway_request_id: Option, +} + #[derive(Clone, Debug)] pub enum GenerateVideoResponse { Artcraft(ArtcraftVideoResponsePayload), Muapi(MuapiVideoResponsePayload), Seedance2Pro(Seedance2proVideoResponsePayload), + Fal(FalVideoResponsePayload), } impl GenerateVideoResponse { @@ -46,4 +53,11 @@ impl GenerateVideoResponse { _ => None, } } + + pub fn get_fal_payload(&self) -> Option { + match self { + Self::Fal(p) => Some(p.clone()), + _ => None, + } + } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/mod.rs new file mode 100644 index 0000000000..64e267315d --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/mod.rs @@ -0,0 +1,5 @@ +pub mod plan_generate_video_fal_veo_2; +pub mod plan_generate_video_fal_veo_3; +pub mod plan_generate_video_fal_veo_3_fast; +pub mod plan_generate_video_fal_veo_3p1; +pub mod plan_generate_video_fal_veo_3p1_fast; 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 new file mode 100644 index 0000000000..0415652a18 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_2.rs @@ -0,0 +1,113 @@ +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::image_ref::ImageRef; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::client_error::ClientError; +use crate::generate::generate_video::generate_video_request::GenerateVideoRequest; +use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan; +use fal_client::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{ + Veo2AspectRatio, Veo2Duration, +}; + +#[derive(Debug, Clone)] +pub struct PlanFalVeo2 { + pub prompt: String, + pub start_frame_url: String, + pub aspect_ratio: Veo2AspectRatio, + pub duration: Veo2Duration, +} + +pub fn plan_generate_video_fal_veo_2<'a>( + request: &'a GenerateVideoRequest<'a>, +) -> Result, ArtcraftRouterError> { + let strategy = request.request_mismatch_mitigation_strategy; + + let start_frame_url = resolve_required_image_url(request.start_frame, "start_frame")?; + if request.end_frame.is_some() { + return Err(unsupported("end_frame", "Veo 2 does not support an ending frame")); + } + + let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?; + let duration = plan_duration(request.duration_seconds, strategy)?; + + Ok(VideoGenerationPlan::FalVeo2(PlanFalVeo2 { + prompt: request.prompt.unwrap_or("").to_string(), + start_frame_url, + aspect_ratio, + duration, + })) +} + +fn resolve_required_image_url( + image_ref: Option>, + field: &'static str, +) -> Result { + match image_ref { + Some(ImageRef::Url(url)) => Ok(url.to_string()), + Some(ImageRef::MediaFileToken(_)) => { + Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls)) + } + None => Err(unsupported(field, "Veo 2 requires a starting frame")), + } +} + +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + use Veo2AspectRatio as Ar; + match aspect_ratio { + None + | Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(Ar::Auto), + + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::WideSixteenNine), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::TallNineSixteen), + + Some(unsupported_ar) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(unsupported("aspect_ratio", &format!("{:?}", unsupported_ar))) + } + _ => Ok(Ar::Auto), + }, + } +} + +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + match duration_seconds { + None => Ok(Veo2Duration::Default), + Some(5) => Ok(Veo2Duration::FiveSeconds), + Some(6) => Ok(Veo2Duration::SixSeconds), + Some(7) => Ok(Veo2Duration::SevenSeconds), + Some(8) => Ok(Veo2Duration::EightSeconds), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(unsupported("duration_seconds", &format!("{}", other))) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Veo2Duration::EightSeconds), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Veo2Duration::FiveSeconds), + }, + } +} + +fn unsupported(field: &'static str, value: &str) -> ArtcraftRouterError { + ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field, + value: value.to_string(), + }) +} + +impl PlanFalVeo2 { + pub fn duration_seconds_for_cost(&self) -> u64 { + match self.duration { + Veo2Duration::Default | Veo2Duration::FiveSeconds => 5, + Veo2Duration::SixSeconds => 6, + Veo2Duration::SevenSeconds => 7, + Veo2Duration::EightSeconds => 8, + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_3.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_3.rs new file mode 100644 index 0000000000..05a1a59e84 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_3.rs @@ -0,0 +1,137 @@ +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::common_resolution::CommonResolution; +use crate::api::image_ref::ImageRef; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::client_error::ClientError; +use crate::generate::generate_video::generate_video_request::GenerateVideoRequest; +use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan; +use fal_client::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{ + Veo3AspectRatio, Veo3Duration, Veo3Resolution, +}; + +#[derive(Debug, Clone)] +pub struct PlanFalVeo3 { + pub prompt: String, + pub start_frame_url: String, + pub aspect_ratio: Veo3AspectRatio, + pub resolution: Veo3Resolution, + pub duration: Veo3Duration, + pub generate_audio: bool, +} + +pub fn plan_generate_video_fal_veo_3<'a>( + request: &'a GenerateVideoRequest<'a>, +) -> Result, ArtcraftRouterError> { + let strategy = request.request_mismatch_mitigation_strategy; + + let start_frame_url = resolve_required_image_url(request.start_frame)?; + if request.end_frame.is_some() { + return Err(unsupported("end_frame", "Veo 3 does not support an ending frame")); + } + + let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?; + let resolution = plan_resolution(request.resolution, strategy)?; + let duration = plan_duration(request.duration_seconds, strategy)?; + let generate_audio = request.generate_audio.unwrap_or(true); + + Ok(VideoGenerationPlan::FalVeo3(PlanFalVeo3 { + prompt: request.prompt.unwrap_or("").to_string(), + start_frame_url, + aspect_ratio, + resolution, + duration, + generate_audio, + })) +} + +fn resolve_required_image_url( + image_ref: Option>, +) -> Result { + match image_ref { + Some(ImageRef::Url(url)) => Ok(url.to_string()), + Some(ImageRef::MediaFileToken(_)) => { + Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls)) + } + None => Err(unsupported("start_frame", "Veo 3 requires a starting frame")), + } +} + +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + use Veo3AspectRatio as Ar; + match aspect_ratio { + None + | Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(Ar::Default), + + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Ar::Square), + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::WideSixteenNine), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::TallNineSixteen), + + Some(unsupported_ar) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(unsupported("aspect_ratio", &format!("{:?}", unsupported_ar))) + } + _ => Ok(Ar::Default), + }, + } +} + +fn plan_resolution( + resolution: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + use Veo3Resolution as R; + match resolution { + None => Ok(R::Default), + Some(CommonResolution::SevenTwentyP) => Ok(R::SevenTwentyP), + Some(CommonResolution::TenEightyP) => Ok(R::TenEightyP), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(unsupported("resolution", &format!("{:?}", other))) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(R::TenEightyP), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(R::SevenTwentyP), + }, + } +} + +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + match duration_seconds { + None => Ok(Veo3Duration::Default), + Some(4) => Ok(Veo3Duration::FourSeconds), + Some(6) => Ok(Veo3Duration::SixSeconds), + Some(8) => Ok(Veo3Duration::EightSeconds), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(unsupported("duration_seconds", &format!("{}", other))) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Veo3Duration::EightSeconds), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Veo3Duration::FourSeconds), + }, + } +} + +fn unsupported(field: &'static str, value: &str) -> ArtcraftRouterError { + ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field, + value: value.to_string(), + }) +} + +impl PlanFalVeo3 { + pub fn duration_seconds_for_cost(&self) -> u64 { + match self.duration { + Veo3Duration::Default | Veo3Duration::EightSeconds => 8, + Veo3Duration::SixSeconds => 6, + Veo3Duration::FourSeconds => 4, + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_3_fast.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_3_fast.rs new file mode 100644 index 0000000000..be3fc3c336 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_3_fast.rs @@ -0,0 +1,109 @@ +use crate::api::common_resolution::CommonResolution; +use crate::api::image_ref::ImageRef; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::client_error::ClientError; +use crate::generate::generate_video::generate_video_request::GenerateVideoRequest; +use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan; +use fal_client::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{ + Veo3FastDuration, Veo3FastResolution, +}; + +#[derive(Debug, Clone)] +pub struct PlanFalVeo3Fast { + pub prompt: String, + pub start_frame_url: String, + pub resolution: Veo3FastResolution, + pub duration: Veo3FastDuration, + pub generate_audio: bool, +} + +pub fn plan_generate_video_fal_veo_3_fast<'a>( + request: &'a GenerateVideoRequest<'a>, +) -> Result, ArtcraftRouterError> { + let strategy = request.request_mismatch_mitigation_strategy; + + let start_frame_url = resolve_required_image_url(request.start_frame)?; + if request.end_frame.is_some() { + return Err(unsupported("end_frame", "Veo 3 Fast does not support an ending frame")); + } + + let resolution = plan_resolution(request.resolution, strategy)?; + let duration = plan_duration(request.duration_seconds, strategy)?; + let generate_audio = request.generate_audio.unwrap_or(true); + + Ok(VideoGenerationPlan::FalVeo3Fast(PlanFalVeo3Fast { + prompt: request.prompt.unwrap_or("").to_string(), + start_frame_url, + resolution, + duration, + generate_audio, + })) +} + +fn resolve_required_image_url( + image_ref: Option>, +) -> Result { + match image_ref { + Some(ImageRef::Url(url)) => Ok(url.to_string()), + Some(ImageRef::MediaFileToken(_)) => { + Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls)) + } + None => Err(unsupported("start_frame", "Veo 3 Fast requires a starting frame")), + } +} + +fn plan_resolution( + resolution: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + use Veo3FastResolution as R; + match resolution { + None => Ok(R::Default), + Some(CommonResolution::SevenTwentyP) => Ok(R::SevenTwentyP), + Some(CommonResolution::TenEightyP) => Ok(R::TenEightyP), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(unsupported("resolution", &format!("{:?}", other))) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(R::TenEightyP), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(R::SevenTwentyP), + }, + } +} + +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + match duration_seconds { + None => Ok(Veo3FastDuration::Default), + Some(4) => Ok(Veo3FastDuration::FourSeconds), + Some(6) => Ok(Veo3FastDuration::SixSeconds), + Some(8) => Ok(Veo3FastDuration::EightSeconds), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(unsupported("duration_seconds", &format!("{}", other))) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Veo3FastDuration::EightSeconds), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Veo3FastDuration::FourSeconds), + }, + } +} + +fn unsupported(field: &'static str, value: &str) -> ArtcraftRouterError { + ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field, + value: value.to_string(), + }) +} + +impl PlanFalVeo3Fast { + pub fn duration_seconds_for_cost(&self) -> u64 { + match self.duration { + Veo3FastDuration::Default | Veo3FastDuration::EightSeconds => 8, + Veo3FastDuration::SixSeconds => 6, + Veo3FastDuration::FourSeconds => 4, + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_3p1.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_3p1.rs new file mode 100644 index 0000000000..20be14c776 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_3p1.rs @@ -0,0 +1,194 @@ +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::common_resolution::CommonResolution; +use crate::api::image_ref::ImageRef; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::client_error::ClientError; +use crate::generate::generate_video::generate_video_request::GenerateVideoRequest; +use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan; + +/// Three-way internal selection of which Fal Veo 3.1 endpoint to call. +/// Veo 3.1 / 3.1 Fast each expose distinct (but parallel) types per endpoint — +/// we resolve those at execute time. +#[derive(Debug, Clone)] +pub enum FalVeo3p1Mode { + TextToVideo, + ImageToVideo { start_frame_url: String }, + FirstLastFrame { first_frame_url: String, last_frame_url: String }, +} + +#[derive(Debug, Clone, Copy)] +pub enum FalVeo3p1AspectRatio { + Auto, + SixteenByNine, + NineBySixteen, +} + +#[derive(Debug, Clone, Copy)] +pub enum FalVeo3p1Resolution { + SevenTwentyP, + TenEightyP, +} + +#[derive(Debug, Clone, Copy)] +pub enum FalVeo3p1Duration { + Four, + Six, + Eight, +} + +#[derive(Debug, Clone)] +pub struct PlanFalVeo3p1 { + pub prompt: String, + pub negative_prompt: Option, + pub mode: FalVeo3p1Mode, + pub aspect_ratio: Option, + pub resolution: Option, + pub duration: Option, + pub generate_audio: Option, +} + +pub fn plan_generate_video_fal_veo_3p1<'a>( + request: &'a GenerateVideoRequest<'a>, +) -> Result, ArtcraftRouterError> { + let plan = build_plan_fal_veo_3p1(request, "Veo 3.1")?; + Ok(VideoGenerationPlan::FalVeo3p1(plan)) +} + +pub(crate) fn build_plan_fal_veo_3p1<'a>( + request: &'a GenerateVideoRequest<'a>, + model_label: &'static str, +) -> Result { + let strategy = request.request_mismatch_mitigation_strategy; + + let mode = resolve_mode(request.start_frame, request.end_frame, model_label)?; + let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?; + let resolution = plan_resolution(request.resolution, strategy)?; + let duration = plan_duration(request.duration_seconds, strategy)?; + + Ok(PlanFalVeo3p1 { + prompt: request.prompt.unwrap_or("").to_string(), + negative_prompt: request.negative_prompt.map(|s| s.to_string()), + mode, + aspect_ratio, + resolution, + duration, + generate_audio: request.generate_audio, + }) +} + +fn resolve_mode( + start_frame: Option>, + end_frame: Option>, + model_label: &'static str, +) -> Result { + let start = resolve_optional_image_url(start_frame)?; + let end = resolve_optional_image_url(end_frame)?; + match (start, end) { + (None, None) => Ok(FalVeo3p1Mode::TextToVideo), + (Some(s), None) => Ok(FalVeo3p1Mode::ImageToVideo { start_frame_url: s }), + (Some(s), Some(e)) => Ok(FalVeo3p1Mode::FirstLastFrame { + first_frame_url: s, + last_frame_url: e, + }), + (None, Some(_)) => Err(unsupported( + "end_frame", + &format!("{} requires a start_frame when end_frame is provided", model_label), + )), + } +} + +fn resolve_optional_image_url( + image_ref: Option>, +) -> Result, ArtcraftRouterError> { + match image_ref { + None => Ok(None), + Some(ImageRef::Url(url)) => Ok(Some(url.to_string())), + Some(ImageRef::MediaFileToken(_)) => { + Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls)) + } + } +} + +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use FalVeo3p1AspectRatio as Ar; + match aspect_ratio { + None => Ok(None), + + Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(Some(Ar::Auto)), + + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Ar::SixteenByNine)), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Ar::NineBySixteen)), + + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(unsupported("aspect_ratio", &format!("{:?}", other))) + } + _ => Ok(Some(Ar::Auto)), + }, + } +} + +fn plan_resolution( + resolution: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use FalVeo3p1Resolution as R; + match resolution { + None => Ok(None), + Some(CommonResolution::SevenTwentyP) => Ok(Some(R::SevenTwentyP)), + Some(CommonResolution::TenEightyP) => Ok(Some(R::TenEightyP)), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(unsupported("resolution", &format!("{:?}", other))) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(R::TenEightyP)), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(R::SevenTwentyP)), + }, + } +} + +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match duration_seconds { + None => Ok(None), + Some(4) => Ok(Some(FalVeo3p1Duration::Four)), + Some(6) => Ok(Some(FalVeo3p1Duration::Six)), + Some(8) => Ok(Some(FalVeo3p1Duration::Eight)), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(unsupported("duration_seconds", &format!("{}", other))) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(FalVeo3p1Duration::Eight)), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(FalVeo3p1Duration::Four)), + }, + } +} + +fn unsupported(field: &'static str, value: &str) -> ArtcraftRouterError { + ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field, + value: value.to_string(), + }) +} + +impl PlanFalVeo3p1 { + pub fn duration_seconds_for_cost(&self) -> u64 { + match self.duration { + Some(FalVeo3p1Duration::Four) => 4, + Some(FalVeo3p1Duration::Six) => 6, + Some(FalVeo3p1Duration::Eight) | None => 8, + } + } + + pub fn generate_audio_for_cost(&self) -> bool { + self.generate_audio.unwrap_or(true) + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_3p1_fast.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_3p1_fast.rs new file mode 100644 index 0000000000..e810847673 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/fal/plan_generate_video_fal_veo_3p1_fast.rs @@ -0,0 +1,21 @@ +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::generate::generate_video::generate_video_request::GenerateVideoRequest; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1::{ + build_plan_fal_veo_3p1, PlanFalVeo3p1, +}; +use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan; + +/// Veo 3.1 Fast shares the exact same input shape as Veo 3.1 — only the +/// downstream Fal endpoint and pricing differ. We reuse the planner and tag the +/// resulting plan as the Fast variant for execute/cost dispatch. +#[derive(Debug, Clone)] +pub struct PlanFalVeo3p1Fast { + pub inner: PlanFalVeo3p1, +} + +pub fn plan_generate_video_fal_veo_3p1_fast<'a>( + request: &'a GenerateVideoRequest<'a>, +) -> Result, ArtcraftRouterError> { + let inner = build_plan_fal_veo_3p1(request, "Veo 3.1 Fast")?; + Ok(VideoGenerationPlan::FalVeo3p1Fast(PlanFalVeo3p1Fast { inner })) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/mod.rs index 881b04ca9a..0674492f66 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/mod.rs @@ -1,3 +1,4 @@ pub mod artcraft; +pub mod fal; pub mod muapi; pub mod seedance2pro; \ No newline at end of file diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_plan.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_plan.rs index 2fd04818ae..fa0ef4d686 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_plan.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_plan.rs @@ -4,12 +4,22 @@ use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraf use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_kling3p0_standard::estimate_video_cost_artcraft_kling3p0_standard; use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance1p5_pro::estimate_video_cost_artcraft_seedance1p5_pro; use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance2p0::estimate_video_cost_artcraft_seedance2p0; +use crate::generate::generate_video::cost::fal::estimate_video_cost_fal_veo_2::estimate_video_cost_fal_veo_2; +use crate::generate::generate_video::cost::fal::estimate_video_cost_fal_veo_3::estimate_video_cost_fal_veo_3; +use crate::generate::generate_video::cost::fal::estimate_video_cost_fal_veo_3_fast::estimate_video_cost_fal_veo_3_fast; +use crate::generate::generate_video::cost::fal::estimate_video_cost_fal_veo_3p1::estimate_video_cost_fal_veo_3p1; +use crate::generate::generate_video::cost::fal::estimate_video_cost_fal_veo_3p1_fast::estimate_video_cost_fal_veo_3p1_fast; use crate::generate::generate_video::cost::muapi::estimate_video_cost_muapi_seedance2p0::estimate_video_cost_muapi_seedance2p0; use crate::generate::generate_video::cost::seedance2pro::estimate_video_cost_seedance2pro_seedance2p0::estimate_video_cost_seedance2pro_seedance2p0; use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling3p0_pro::execute_artcraft_kling3p0_pro; use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling3p0_standard::execute_artcraft_kling3p0_standard; use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_seedance1p5_pro::execute_artcraft_seedance1p5_pro; use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_seedance2p0::execute_artcraft_seedance2p0; +use crate::generate::generate_video::execute::fal::generate_video_fal_veo_2::execute_fal_veo_2; +use crate::generate::generate_video::execute::fal::generate_video_fal_veo_3::execute_fal_veo_3; +use crate::generate::generate_video::execute::fal::generate_video_fal_veo_3_fast::execute_fal_veo_3_fast; +use crate::generate::generate_video::execute::fal::generate_video_fal_veo_3p1::execute_fal_veo_3p1; +use crate::generate::generate_video::execute::fal::generate_video_fal_veo_3p1_fast::execute_fal_veo_3p1_fast; use crate::generate::generate_video::execute::muapi::execute_muapi_seedance2p0::execute_muapi_seedance2p0; use crate::generate::generate_video::execute::seedance2pro::execute_seedance2pro_seedance2p0::execute_seedance2pro_seedance2p0; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; @@ -17,6 +27,11 @@ use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraf use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::PlanArtcraftKling3p0Standard; use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance1p5_pro::PlanArtcraftSeedance1p5Pro; use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_2::PlanFalVeo2; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3::PlanFalVeo3; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3_fast::PlanFalVeo3Fast; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1::PlanFalVeo3p1; +use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1_fast::PlanFalVeo3p1Fast; use crate::generate::generate_video::plan::muapi::plan_generate_video_muapi_seedance2p0::PlanMuapiSeedance2p0; use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::PlanSeedance2proSeedance2p0; use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; @@ -29,6 +44,11 @@ pub enum VideoGenerationPlan<'a> { ArtcraftSeedance2p0(PlanArtcraftSeedance2p0<'a>), MuapiSeedance2p0(PlanMuapiSeedance2p0), Seedance2proSeedance2p0(PlanSeedance2proSeedance2p0), + FalVeo2(PlanFalVeo2), + FalVeo3(PlanFalVeo3), + FalVeo3Fast(PlanFalVeo3Fast), + FalVeo3p1(PlanFalVeo3p1), + FalVeo3p1Fast(PlanFalVeo3p1Fast), } impl<'a> VideoGenerationPlan<'a> { @@ -61,6 +81,26 @@ impl<'a> VideoGenerationPlan<'a> { let seedance2pro_client = client.get_seedance2pro_client_ref()?; execute_seedance2pro_seedance2p0(plan, seedance2pro_client).await } + VideoGenerationPlan::FalVeo2(plan) => { + let fal_client = client.get_fal_client_ref()?; + execute_fal_veo_2(plan, fal_client).await + } + VideoGenerationPlan::FalVeo3(plan) => { + let fal_client = client.get_fal_client_ref()?; + execute_fal_veo_3(plan, fal_client).await + } + VideoGenerationPlan::FalVeo3Fast(plan) => { + let fal_client = client.get_fal_client_ref()?; + execute_fal_veo_3_fast(plan, fal_client).await + } + VideoGenerationPlan::FalVeo3p1(plan) => { + let fal_client = client.get_fal_client_ref()?; + execute_fal_veo_3p1(plan, fal_client).await + } + VideoGenerationPlan::FalVeo3p1Fast(plan) => { + let fal_client = client.get_fal_client_ref()?; + execute_fal_veo_3p1_fast(plan, fal_client).await + } } } @@ -84,6 +124,11 @@ impl<'a> VideoGenerationPlan<'a> { VideoGenerationPlan::Seedance2proSeedance2p0(plan) => { estimate_video_cost_seedance2pro_seedance2p0(plan) } + VideoGenerationPlan::FalVeo2(plan) => estimate_video_cost_fal_veo_2(plan), + VideoGenerationPlan::FalVeo3(plan) => estimate_video_cost_fal_veo_3(plan), + VideoGenerationPlan::FalVeo3Fast(plan) => estimate_video_cost_fal_veo_3_fast(plan), + VideoGenerationPlan::FalVeo3p1(plan) => estimate_video_cost_fal_veo_3p1(plan), + VideoGenerationPlan::FalVeo3p1Fast(plan) => estimate_video_cost_fal_veo_3p1_fast(plan), } } } diff --git a/crates/service/web/storyteller_web/src/configs/omni_gen/video_models.rs b/crates/service/web/storyteller_web/src/configs/omni_gen/video_models.rs index da0d92335c..55f565b998 100644 --- a/crates/service/web/storyteller_web/src/configs/omni_gen/video_models.rs +++ b/crates/service/web/storyteller_web/src/configs/omni_gen/video_models.rs @@ -80,6 +80,111 @@ fn build_omni_gen_video_models() -> Vec { ..Default::default() }); + models.push(OmniGenVideoModelDetails { + model: CommonVideoModel::Veo2, + full_name: Some("Google Veo 2".to_string()), + text_prompt_supported: Some(true), + starting_keyframe_supported: Some(true), + aspect_ratio_options: Some(vec![ + CommonAspectRatio::Auto, + CommonAspectRatio::WideSixteenByNine, + CommonAspectRatio::TallNineBySixteen, + ]), + aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine), + duration_seconds_min: Some(5), + duration_seconds_max: Some(8), + duration_seconds_default: Some(5), + ..Default::default() + }); + + models.push(OmniGenVideoModelDetails { + model: CommonVideoModel::Veo3, + full_name: Some("Google Veo 3".to_string()), + text_prompt_supported: Some(true), + starting_keyframe_supported: Some(true), + show_generate_with_sound_toggle: Some(true), + aspect_ratio_options: Some(vec![ + CommonAspectRatio::WideSixteenByNine, + CommonAspectRatio::TallNineBySixteen, + CommonAspectRatio::Square, + ]), + aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine), + resolution_options: Some(vec![ + CommonResolution::SevenTwentyP, + CommonResolution::TenEightyP, + ]), + resolution_default: Some(CommonResolution::TenEightyP), + duration_seconds_min: Some(4), + duration_seconds_max: Some(8), + duration_seconds_default: Some(8), + ..Default::default() + }); + + models.push(OmniGenVideoModelDetails { + model: CommonVideoModel::Veo3Fast, + full_name: Some("Google Veo 3 Fast".to_string()), + text_prompt_supported: Some(true), + starting_keyframe_supported: Some(true), + show_generate_with_sound_toggle: Some(true), + resolution_options: Some(vec![ + CommonResolution::SevenTwentyP, + CommonResolution::TenEightyP, + ]), + resolution_default: Some(CommonResolution::TenEightyP), + duration_seconds_min: Some(4), + duration_seconds_max: Some(8), + duration_seconds_default: Some(8), + ..Default::default() + }); + + models.push(OmniGenVideoModelDetails { + model: CommonVideoModel::Veo3p1, + full_name: Some("Google Veo 3.1".to_string()), + text_prompt_supported: Some(true), + starting_keyframe_supported: Some(true), + ending_keyframe_supported: Some(true), + show_generate_with_sound_toggle: Some(true), + aspect_ratio_options: Some(vec![ + CommonAspectRatio::Auto, + CommonAspectRatio::WideSixteenByNine, + CommonAspectRatio::TallNineBySixteen, + ]), + aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine), + resolution_options: Some(vec![ + CommonResolution::SevenTwentyP, + CommonResolution::TenEightyP, + ]), + resolution_default: Some(CommonResolution::TenEightyP), + duration_seconds_min: Some(4), + duration_seconds_max: Some(8), + duration_seconds_default: Some(8), + ..Default::default() + }); + + models.push(OmniGenVideoModelDetails { + model: CommonVideoModel::Veo3p1Fast, + full_name: Some("Google Veo 3.1 Fast".to_string()), + text_prompt_supported: Some(true), + starting_keyframe_supported: Some(true), + ending_keyframe_supported: Some(true), + show_generate_with_sound_toggle: Some(true), + aspect_ratio_options: Some(vec![ + CommonAspectRatio::Auto, + CommonAspectRatio::WideSixteenByNine, + CommonAspectRatio::TallNineBySixteen, + ]), + aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine), + resolution_options: Some(vec![ + CommonResolution::SevenTwentyP, + CommonResolution::TenEightyP, + ]), + resolution_default: Some(CommonResolution::TenEightyP), + duration_seconds_min: Some(4), + duration_seconds_max: Some(8), + duration_seconds_default: Some(8), + ..Default::default() + }); + models } @@ -97,6 +202,26 @@ fn build_omni_gen_video_model_providers() -> Vec plan, + Err(ArtcraftRouterError::UnsupportedModel(_)) => { + generate_request.provider = Provider::Fal; + generate_request.build() + .map_err(|e| { + warn!("Failed to build Fal cost plan: {}", e); + AdvancedCommonWebError::from_error(e) + })? + } + Err(e) => { warn!("Failed to build cost plan: {}", e); - AdvancedCommonWebError::from_error(e) - })?; + return Err(AdvancedCommonWebError::from_error(e)); + } + }; let estimate = plan.estimate_costs(); diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs index 899f14f786..6fc91d0218 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs @@ -167,6 +167,9 @@ pub async fn omni_gen_video_generate_handler( artcraft_router::generate::generate_video::generate_video_response::GenerateVideoResponse::Seedance2Pro(p) => { p.order_id.clone() } + artcraft_router::generate::generate_video::generate_video_response::GenerateVideoResponse::Fal(p) => { + p.request_id.clone().unwrap_or_default() + } }; // ==================== DB TRANSACTION ==================== // diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/request_to_costs.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/request_to_costs.rs index d3205b1d2b..e9d72ce2d6 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/request_to_costs.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/request_to_costs.rs @@ -1,4 +1,5 @@ use artcraft_router::api::provider::Provider; +use artcraft_router::errors::artcraft_router_error::ArtcraftRouterError; use artcraft_router::generate::generate_video::generate_video_request::GenerateVideoRequest; use artcraft_router::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; use log::warn; @@ -6,20 +7,33 @@ use log::warn; use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; /// Estimate costs for a video generation request. -/// Always uses the Artcraft provider for costing regardless of the execution provider. +/// Prefers the Artcraft provider for costing, but falls back to Fal for models +/// that are only available via Fal (e.g. Veo). pub fn request_to_costs( request: &GenerateVideoRequest<'_>, ) -> Result { - let mut cost_request = GenerateVideoRequest { + let artcraft_request = GenerateVideoRequest { provider: Provider::Artcraft, ..*request }; - let plan = cost_request.build() - .map_err(|e| { + match artcraft_request.build() { + Ok(plan) => Ok(plan.estimate_costs()), + Err(ArtcraftRouterError::UnsupportedModel(_)) => { + let fal_request = GenerateVideoRequest { + provider: Provider::Fal, + ..*request + }; + let plan = fal_request.build() + .map_err(|e| { + warn!("Failed to build Fal cost plan: {}", e); + AdvancedCommonWebError::from_error(e) + })?; + Ok(plan.estimate_costs()) + } + Err(e) => { warn!("Failed to build cost plan: {}", e); - AdvancedCommonWebError::from_error(e) - })?; - - Ok(plan.estimate_costs()) + Err(AdvancedCommonWebError::from_error(e)) + } + } }