veo endpoints

This commit is contained in:
Brandon Thomas committed 2026-04-07 05:25:10 -04:00
1 parent 9c79025044
commit f1b22b564d
28 files changed
+1346 -12

No files matched your search

@@ -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,
}
}
@@ -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,
}
}
@@ -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,
}
}
@@ -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,
}
}
@@ -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,
}
}
@@ -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;
@@ -1,3 +1,4 @@
pub (crate) mod artcraft;
pub (crate) mod fal;
pub (crate) mod muapi;
pub (crate) mod seedance2pro;
@@ -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<GenerateVideoResponse, ArtcraftRouterError> {
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,
}))
}
@@ -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<GenerateVideoResponse, ArtcraftRouterError> {
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,
}))
}
@@ -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<GenerateVideoResponse, ArtcraftRouterError> {
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,
}))
}
@@ -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<GenerateVideoResponse, ArtcraftRouterError> {
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,
}
}
@@ -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<GenerateVideoResponse, ArtcraftRouterError> {
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,
}
}
@@ -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;
@@ -1,3 +1,4 @@
pub mod artcraft;
pub mod fal;
pub mod muapi;
pub mod seedance2pro;
@@ -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<VideoGenerationPlan<'_>, 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<VideoGenerationPlan<'_>, 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<VideoGenerationPlan<'_>, ArtcraftRouterError> {
match self.model {
CommonVideoModel::Seedance2p0 => plan_generate_video_muapi_seedance2p0(self),
@@ -18,11 +18,18 @@ pub struct Seedance2proVideoResponsePayload {
pub task_id: String,
}
#[derive(Clone, Debug)]
pub struct FalVideoResponsePayload {
pub request_id: Option<String>,
pub gateway_request_id: Option<String>,
}
#[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<FalVideoResponsePayload> {
match self {
Self::Fal(p) => Some(p.clone()),
_ => None,
}
}
}
@@ -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;
@@ -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<VideoGenerationPlan<'a>, 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<ImageRef<'_>>,
field: &'static str,
) -> Result<String, ArtcraftRouterError> {
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<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Veo2AspectRatio, ArtcraftRouterError> {
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<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Veo2Duration, ArtcraftRouterError> {
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,
}
}
}
@@ -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<VideoGenerationPlan<'a>, 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<ImageRef<'_>>,
) -> Result<String, ArtcraftRouterError> {
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<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Veo3AspectRatio, ArtcraftRouterError> {
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<CommonResolution>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Veo3Resolution, ArtcraftRouterError> {
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<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Veo3Duration, ArtcraftRouterError> {
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,
}
}
}
@@ -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<VideoGenerationPlan<'a>, 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<ImageRef<'_>>,
) -> Result<String, ArtcraftRouterError> {
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<CommonResolution>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Veo3FastResolution, ArtcraftRouterError> {
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<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Veo3FastDuration, ArtcraftRouterError> {
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,
}
}
}
@@ -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<String>,
pub mode: FalVeo3p1Mode,
pub aspect_ratio: Option<FalVeo3p1AspectRatio>,
pub resolution: Option<FalVeo3p1Resolution>,
pub duration: Option<FalVeo3p1Duration>,
pub generate_audio: Option<bool>,
}
pub fn plan_generate_video_fal_veo_3p1<'a>(
request: &'a GenerateVideoRequest<'a>,
) -> Result<VideoGenerationPlan<'a>, 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<PlanFalVeo3p1, ArtcraftRouterError> {
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<ImageRef<'_>>,
end_frame: Option<ImageRef<'_>>,
model_label: &'static str,
) -> Result<FalVeo3p1Mode, ArtcraftRouterError> {
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<ImageRef<'_>>,
) -> Result<Option<String>, 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<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<FalVeo3p1AspectRatio>, 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<CommonResolution>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<FalVeo3p1Resolution>, 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<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<FalVeo3p1Duration>, 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)
}
}
@@ -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<VideoGenerationPlan<'a>, ArtcraftRouterError> {
let inner = build_plan_fal_veo_3p1(request, "Veo 3.1 Fast")?;
Ok(VideoGenerationPlan::FalVeo3p1Fast(PlanFalVeo3p1Fast { inner }))
}
@@ -1,3 +1,4 @@
pub mod artcraft;
pub mod fal;
pub mod muapi;
pub mod seedance2pro;
@@ -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),
}
}
}
@@ -80,6 +80,111 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
..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<OmniGenVideoModelProviderDetail
model: CommonVideoModel::Seedance2p0,
overrides: None,
},
OmniGenVideoProviderModelDetails {
model: CommonVideoModel::Veo2,
overrides: None,
},
OmniGenVideoProviderModelDetails {
model: CommonVideoModel::Veo3,
overrides: None,
},
OmniGenVideoProviderModelDetails {
model: CommonVideoModel::Veo3Fast,
overrides: None,
},
OmniGenVideoProviderModelDetails {
model: CommonVideoModel::Veo3p1,
overrides: None,
},
OmniGenVideoProviderModelDetails {
model: CommonVideoModel::Veo3p1Fast,
overrides: None,
},
],
});
@@ -5,6 +5,7 @@ use actix_web::{web, HttpRequest};
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
use artcraft_api_defs::omni_gen::cost_response::omni_gen_video_cost_response::OmniGenVideoCostResponse;
use artcraft_router::api::provider::Provider;
use artcraft_router::errors::artcraft_router_error::ArtcraftRouterError;
use log::warn;
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
@@ -31,11 +32,21 @@ pub async fn omni_gen_video_cost_handler(
let mut generate_request = transform_request(&request)?;
generate_request.provider = Provider::Artcraft;
let plan = generate_request.build()
.map_err(|e| {
let plan = match generate_request.build() {
Ok(plan) => 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();
@@ -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 ==================== //
@@ -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<VideoGenerationCostEstimate, AdvancedCommonWebError> {
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))
}
}
}