mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-25 18:49:38 +00:00
implement fal seedance 1.0 / 1.5
This commit is contained in:
+8
@@ -62,6 +62,8 @@ use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::build::
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::build::build_gmicloud_seedance_2p0_u;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::build::build_gmicloud_seedance_2p0_u_fast;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video::build::build_grok_api_grok_imagine_video;
|
||||
use crate::generate::generate_video_v2::providers::fal::seedance_1p0_lite::build::build_fal_seedance_1p0_lite;
|
||||
use crate::generate::generate_video_v2::providers::fal::seedance_1p5_pro::build::build_fal_seedance_1p5_pro;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
@@ -165,6 +167,9 @@ impl GenerateVideoRequestBuilder {
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0BytePlusFast) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::PreviewModel) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::PreviewModelFast) => true,
|
||||
// Fal
|
||||
(Provider::Fal, CommonVideoModel::Seedance10Lite) => true,
|
||||
(Provider::Fal, CommonVideoModel::Seedance1p5Pro) => true,
|
||||
// GmiCloud
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0Ultra) => true,
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0UltraFast) => true,
|
||||
@@ -192,6 +197,9 @@ impl GenerateVideoRequestBuilder {
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0BytePlusFast) => build_artcraft_seedance_2p0_bp_fast(self),
|
||||
(Provider::Artcraft, CommonVideoModel::PreviewModel) => build_artcraft_preview_model(self),
|
||||
(Provider::Artcraft, CommonVideoModel::PreviewModelFast) => build_artcraft_preview_model_fast(self),
|
||||
// Fal
|
||||
(Provider::Fal, CommonVideoModel::Seedance10Lite) => build_fal_seedance_1p0_lite(self),
|
||||
(Provider::Fal, CommonVideoModel::Seedance1p5Pro) => build_fal_seedance_1p5_pro(self),
|
||||
// GmiCloud
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0Ultra) => build_gmicloud_seedance_2p0_u(self),
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0UltraFast) => build_gmicloud_seedance_2p0_u_fast(self),
|
||||
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
//! Combinatorial cost-parity tests between v1 (`build().estimate_cost()`) and v2
|
||||
//! (`build2().estimate_cost()`) for the Fal-hosted Seedance video models.
|
||||
//!
|
||||
//! The goal: every request that produces a price in v1 must produce the
|
||||
//! identical price in v2. If any callsite picks a different price after the
|
||||
//! port, billing diverges. These tests close that gap.
|
||||
//!
|
||||
//! Both pipelines ultimately delegate to the same `FalRequestCostCalculator`
|
||||
//! impls in `fal_client`, so parity should hold by construction — but we
|
||||
//! exercise a wide variety of inputs anyway to catch any drift in the request
|
||||
//! plumbing (e.g. resolution/duration mapping errors, default-value mismatches,
|
||||
//! aspect-ratio fallthroughs).
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
const DUMMY_IMAGE: &str = "https://example.com/dummy.png";
|
||||
const DUMMY_END_FRAME: &str = "https://example.com/dummy_end.png";
|
||||
|
||||
// ── seedance 1.0 lite ──
|
||||
|
||||
mod seedance_1p0_lite {
|
||||
use super::*;
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance10Lite,
|
||||
provider: Provider::Fal,
|
||||
// 1.0 lite is image-to-video only — a start_frame is required.
|
||||
start_frame: Some(ImageRef::Url(DUMMY_IMAGE.to_string())),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn all_resolutions() -> &'static [Option<CommonResolution>] {
|
||||
&[
|
||||
None,
|
||||
Some(CommonResolution::FourEightyP),
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
Some(CommonResolution::TenEightyP),
|
||||
]
|
||||
}
|
||||
|
||||
fn all_durations() -> &'static [Option<u16>] {
|
||||
&[None, Some(5), Some(10)]
|
||||
}
|
||||
|
||||
fn supported_aspect_ratios() -> &'static [Option<CommonAspectRatio>] {
|
||||
&[
|
||||
None,
|
||||
Some(CommonAspectRatio::Auto),
|
||||
Some(CommonAspectRatio::Square),
|
||||
Some(CommonAspectRatio::WideFourByThree),
|
||||
Some(CommonAspectRatio::WideSixteenByNine),
|
||||
Some(CommonAspectRatio::Wide),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine),
|
||||
Some(CommonAspectRatio::TallThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineBySixteen),
|
||||
Some(CommonAspectRatio::Tall),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_parity_full_combinatorial() {
|
||||
let mut combos_checked = 0;
|
||||
let mut combos_with_end_frame = 0;
|
||||
|
||||
for &resolution in all_resolutions() {
|
||||
for &duration in all_durations() {
|
||||
for &aspect_ratio in supported_aspect_ratios() {
|
||||
for include_end_frame in [false, true] {
|
||||
let mut builder = base_builder();
|
||||
builder.resolution = resolution;
|
||||
builder.duration_seconds = duration;
|
||||
builder.aspect_ratio = aspect_ratio;
|
||||
if include_end_frame {
|
||||
builder.end_frame = Some(ImageRef::Url(DUMMY_END_FRAME.to_string()));
|
||||
combos_with_end_frame += 1;
|
||||
}
|
||||
|
||||
let v1 = v1_cost(&builder);
|
||||
let v2 = v2_cost(builder.clone());
|
||||
assert_eq!(
|
||||
v1, v2,
|
||||
"1.0 lite cost mismatch: res={:?} dur={:?} ar={:?} end={} → v1={:?} v2={:?}",
|
||||
resolution, duration, aspect_ratio, include_end_frame, v1, v2,
|
||||
);
|
||||
combos_checked += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity: we should be checking a meaningful number of combinations.
|
||||
assert!(combos_checked >= 240, "expected ≥240 combos, got {}", combos_checked);
|
||||
assert!(combos_with_end_frame > 0, "expected at least one with end_frame");
|
||||
}
|
||||
}
|
||||
|
||||
// ── seedance 1.5 pro ──
|
||||
|
||||
mod seedance_1p5_pro {
|
||||
use super::*;
|
||||
|
||||
fn t2v_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance1p5Pro,
|
||||
provider: Provider::Fal,
|
||||
// No start_frame → text-to-video mode.
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn i2v_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance1p5Pro,
|
||||
provider: Provider::Fal,
|
||||
start_frame: Some(ImageRef::Url(DUMMY_IMAGE.to_string())),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn all_resolutions() -> &'static [Option<CommonResolution>] {
|
||||
&[
|
||||
None,
|
||||
Some(CommonResolution::FourEightyP),
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
Some(CommonResolution::TenEightyP),
|
||||
]
|
||||
}
|
||||
|
||||
fn all_durations() -> &'static [Option<u16>] {
|
||||
&[None, Some(4), Some(5), Some(6), Some(7), Some(8), Some(9), Some(10), Some(11), Some(12)]
|
||||
}
|
||||
|
||||
fn supported_aspect_ratios() -> &'static [Option<CommonAspectRatio>] {
|
||||
&[
|
||||
None,
|
||||
Some(CommonAspectRatio::Auto),
|
||||
Some(CommonAspectRatio::Square),
|
||||
Some(CommonAspectRatio::WideFourByThree),
|
||||
Some(CommonAspectRatio::WideSixteenByNine),
|
||||
Some(CommonAspectRatio::Wide),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine),
|
||||
Some(CommonAspectRatio::TallThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineBySixteen),
|
||||
Some(CommonAspectRatio::Tall),
|
||||
]
|
||||
}
|
||||
|
||||
fn audio_options() -> &'static [Option<bool>] {
|
||||
&[None, Some(true), Some(false)]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_parity_t2v_combinatorial() {
|
||||
let mut combos_checked = 0;
|
||||
for &resolution in all_resolutions() {
|
||||
for &duration in all_durations() {
|
||||
for &aspect_ratio in supported_aspect_ratios() {
|
||||
for &generate_audio in audio_options() {
|
||||
let mut builder = t2v_builder();
|
||||
builder.resolution = resolution;
|
||||
builder.duration_seconds = duration;
|
||||
builder.aspect_ratio = aspect_ratio;
|
||||
builder.generate_audio = generate_audio;
|
||||
|
||||
let v1 = v1_cost(&builder);
|
||||
let v2 = v2_cost(builder.clone());
|
||||
assert_eq!(
|
||||
v1, v2,
|
||||
"1.5 pro t2v cost mismatch: res={:?} dur={:?} ar={:?} audio={:?} → v1={:?} v2={:?}",
|
||||
resolution, duration, aspect_ratio, generate_audio, v1, v2,
|
||||
);
|
||||
combos_checked += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(combos_checked >= 1000, "expected ≥1000 combos, got {}", combos_checked);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_parity_i2v_combinatorial() {
|
||||
let mut combos_checked = 0;
|
||||
let mut combos_with_end_frame = 0;
|
||||
for &resolution in all_resolutions() {
|
||||
for &duration in all_durations() {
|
||||
for &aspect_ratio in supported_aspect_ratios() {
|
||||
for &generate_audio in audio_options() {
|
||||
for include_end_frame in [false, true] {
|
||||
let mut builder = i2v_builder();
|
||||
builder.resolution = resolution;
|
||||
builder.duration_seconds = duration;
|
||||
builder.aspect_ratio = aspect_ratio;
|
||||
builder.generate_audio = generate_audio;
|
||||
if include_end_frame {
|
||||
builder.end_frame = Some(ImageRef::Url(DUMMY_END_FRAME.to_string()));
|
||||
combos_with_end_frame += 1;
|
||||
}
|
||||
|
||||
let v1 = v1_cost(&builder);
|
||||
let v2 = v2_cost(builder.clone());
|
||||
assert_eq!(
|
||||
v1, v2,
|
||||
"1.5 pro i2v cost mismatch: res={:?} dur={:?} ar={:?} audio={:?} end={} → v1={:?} v2={:?}",
|
||||
resolution, duration, aspect_ratio, generate_audio, include_end_frame, v1, v2,
|
||||
);
|
||||
combos_checked += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(combos_checked >= 2000, "expected ≥2000 combos, got {}", combos_checked);
|
||||
assert!(combos_with_end_frame > 0, "expected at least one with end_frame");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_parity_with_pay_less_downgrade_strategy() {
|
||||
// Sanity: even when the mismatch strategy downgrades unsupported
|
||||
// resolutions/durations, v1 and v2 should pick the same fallback.
|
||||
let oddities: &[(Option<CommonResolution>, Option<u16>)] = &[
|
||||
(Some(CommonResolution::OneK), None),
|
||||
(Some(CommonResolution::TwoK), Some(5)),
|
||||
(Some(CommonResolution::FourK), Some(12)),
|
||||
(None, Some(13)),
|
||||
];
|
||||
for &(resolution, duration) in oddities {
|
||||
let mut builder = t2v_builder();
|
||||
builder.resolution = resolution;
|
||||
builder.duration_seconds = duration;
|
||||
builder.request_mismatch_mitigation_strategy =
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade;
|
||||
let v1 = v1_cost(&builder);
|
||||
let v2 = v2_cost(builder.clone());
|
||||
assert_eq!(v1, v2, "PayLessDowngrade mismatch at res={:?} dur={:?}", resolution, duration);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_parity_with_pay_more_upgrade_strategy() {
|
||||
let oddities: &[(Option<CommonResolution>, Option<u16>)] = &[
|
||||
(Some(CommonResolution::OneK), None),
|
||||
(Some(CommonResolution::TwoK), Some(5)),
|
||||
(Some(CommonResolution::FourK), Some(13)),
|
||||
(None, Some(20)),
|
||||
];
|
||||
for &(resolution, duration) in oddities {
|
||||
let mut builder = t2v_builder();
|
||||
builder.resolution = resolution;
|
||||
builder.duration_seconds = duration;
|
||||
builder.request_mismatch_mitigation_strategy =
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade;
|
||||
let v1 = v1_cost(&builder);
|
||||
let v2 = v2_cost(builder.clone());
|
||||
assert_eq!(v1, v2, "PayMoreUpgrade mismatch at res={:?} dur={:?}", resolution, duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── helpers ──
|
||||
|
||||
/// Returns `Some(cost_in_usd_cents)` from the v1 pipeline. If v1 errors out
|
||||
/// (e.g. for an unsupported resolution with `ErrorOut` strategy), returns `None`
|
||||
/// so the parity test can compare apples to apples.
|
||||
fn v1_cost(builder: &GenerateVideoRequestBuilder) -> Option<u64> {
|
||||
builder
|
||||
.build()
|
||||
.ok()
|
||||
.and_then(|plan| plan.estimate_costs().cost_in_usd_cents)
|
||||
}
|
||||
|
||||
/// Same as `v1_cost`, but for the v2 pipeline.
|
||||
fn v2_cost(builder: GenerateVideoRequestBuilder) -> Option<u64> {
|
||||
builder
|
||||
.build2()
|
||||
.ok()
|
||||
.and_then(|dor| dor.estimate_cost().ok())
|
||||
.and_then(|estimate| estimate.cost_in_usd_cents)
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
pub mod seedance_1p0_lite;
|
||||
pub mod seedance_1p5_pro;
|
||||
|
||||
#[cfg(test)]
|
||||
mod cost_parity_tests;
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
use fal_client::requests::webhook::video::image::enqueue_seedance_1_lite_image_to_video_webhook::{
|
||||
Seedance1LiteAspectRatio, Seedance1LiteDuration, Seedance1LiteRequest, Seedance1LiteResolution,
|
||||
};
|
||||
|
||||
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_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::fal::seedance_1p0_lite::request::FalSeedance10LiteRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_fal_seedance_1p0_lite(
|
||||
mut builder: GenerateVideoRequestBuilder,
|
||||
) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
let image_url = require_url(
|
||||
builder.start_frame.take(),
|
||||
"start_frame",
|
||||
"Seedance 1.0 Lite requires a starting frame",
|
||||
)?;
|
||||
let end_frame_image_url = optional_url(builder.end_frame.take())?;
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
|
||||
let resolution = plan_resolution(builder.resolution.take(), strategy)?;
|
||||
let duration = plan_duration(builder.duration_seconds.take(), strategy)?;
|
||||
let prompt = builder.prompt.take().unwrap_or_default();
|
||||
|
||||
let request = Seedance1LiteRequest {
|
||||
image_url,
|
||||
end_frame_image_url,
|
||||
prompt,
|
||||
duration,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
camera_fixed: false,
|
||||
seed: None,
|
||||
};
|
||||
|
||||
let state = FalSeedance10LiteRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalSeedance10Lite(state)))
|
||||
}
|
||||
|
||||
// ── Plan helpers (copies of v1 logic; kept in sync intentionally) ──
|
||||
|
||||
fn require_url(
|
||||
start_frame: Option<ImageRef>,
|
||||
field: &'static str,
|
||||
msg: &'static str,
|
||||
) -> Result<String, ArtcraftRouterError> {
|
||||
match start_frame {
|
||||
Some(ImageRef::Url(url)) => Ok(url),
|
||||
Some(ImageRef::MediaFileToken(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field,
|
||||
value: "Fal only supports image URLs, not media file tokens".to_string(),
|
||||
}))
|
||||
}
|
||||
None => Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field,
|
||||
value: msg.to_string(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_url(image_ref: Option<ImageRef>) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
match image_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageRef::Url(url)) => Ok(Some(url)),
|
||||
Some(ImageRef::MediaFileToken(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "end_frame",
|
||||
value: "Fal only supports image URLs, not media file tokens".to_string(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Seedance1LiteAspectRatio>, ArtcraftRouterError> {
|
||||
use Seedance1LiteAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Some(Ar::Auto)),
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(Ar::Square)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(Ar::FourByThree)),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Ar::SixteenByNine)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(Ar::TwentyOneByNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(Ar::ThreeByFour)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Ar::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(Ar::Auto)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Seedance1LiteResolution, ArtcraftRouterError> {
|
||||
use Seedance1LiteResolution as R;
|
||||
match resolution {
|
||||
None => Ok(R::SevenTwentyP),
|
||||
Some(CommonResolution::FourEightyP) => Ok(R::FourEightyP),
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(R::SevenTwentyP),
|
||||
Some(CommonResolution::TenEightyP) => Ok(R::TenEightyP),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", other),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(R::TenEightyP),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(R::FourEightyP),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Seedance1LiteDuration, ArtcraftRouterError> {
|
||||
match duration_seconds {
|
||||
None => Ok(Seedance1LiteDuration::FiveSeconds),
|
||||
Some(5) => Ok(Seedance1LiteDuration::FiveSeconds),
|
||||
Some(10) => Ok(Seedance1LiteDuration::TenSeconds),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", other),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Seedance1LiteDuration::TenSeconds),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Seedance1LiteDuration::FiveSeconds),
|
||||
},
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::fal::seedance_1p0_lite::request::FalSeedance10LiteRequestState;
|
||||
|
||||
pub struct FalSeedance10LiteCostState {
|
||||
request: FalSeedance10LiteRequestState,
|
||||
}
|
||||
|
||||
impl FalSeedance10LiteCostState {
|
||||
pub fn from_request(request: &FalSeedance10LiteRequestState) -> Self {
|
||||
Self { request: request.clone() }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
// Mirrors v1 — delegate to the Fal client's cost calculator for billing parity.
|
||||
let cost_in_usd_cents = self.request.request.calculate_cost_in_cents();
|
||||
|
||||
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,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::webhook::video::image::enqueue_seedance_1_lite_image_to_video_webhook::{
|
||||
enqueue_seedance_1_lite_image_to_video_webhook, Seedance1LiteArgs, Seedance1LiteRequest,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalSeedance10LiteRequestState {
|
||||
pub request: Seedance1LiteRequest,
|
||||
}
|
||||
|
||||
impl FalSeedance10LiteRequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let outbound_request: Arc<dyn Debug + Send + Sync> = Arc::new(self.request.clone());
|
||||
|
||||
let args = Seedance1LiteArgs {
|
||||
request: self.request.clone(),
|
||||
api_key: &client.api_key,
|
||||
webhook_url: client.webhook_url.as_str(),
|
||||
};
|
||||
|
||||
let webhook_response = enqueue_seedance_1_lite_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,
|
||||
maybe_outbound_request: Some(outbound_request),
|
||||
}))
|
||||
}
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
use fal_client::requests::webhook::video::image::enqueue_seedance_1p5_pro_image_to_video_webhook::{
|
||||
EnqueueSeedance1p5ProImageToVideoAspectRatio, EnqueueSeedance1p5ProImageToVideoDuration,
|
||||
EnqueueSeedance1p5ProImageToVideoRequest, EnqueueSeedance1p5ProImageToVideoResolution,
|
||||
};
|
||||
use fal_client::requests::webhook::video::text::enqueue_seedance_1p5_pro_text_to_video_webhook::{
|
||||
EnqueueSeedance1p5ProTextToVideoAspectRatio, EnqueueSeedance1p5ProTextToVideoDuration,
|
||||
EnqueueSeedance1p5ProTextToVideoRequest, EnqueueSeedance1p5ProTextToVideoResolution,
|
||||
};
|
||||
|
||||
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_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::fal::seedance_1p5_pro::request::{
|
||||
FalSeedance1p5ProMode, FalSeedance1p5ProRequestState,
|
||||
};
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
// Resolution / duration / aspect-ratio variants used for planning. Mirrors the
|
||||
// v1 plan types, but lives entirely inside this module so the v2 dir is self-contained.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
enum PlanResolution {
|
||||
FourEightyP,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
enum PlanDuration {
|
||||
Four,
|
||||
Five,
|
||||
Six,
|
||||
Seven,
|
||||
Eight,
|
||||
Nine,
|
||||
Ten,
|
||||
Eleven,
|
||||
Twelve,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
enum PlanAspectRatio {
|
||||
Auto,
|
||||
TwentyOneByNine,
|
||||
SixteenByNine,
|
||||
FourByThree,
|
||||
Square,
|
||||
ThreeByFour,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
pub fn build_fal_seedance_1p5_pro(
|
||||
mut builder: GenerateVideoRequestBuilder,
|
||||
) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Decide t2v vs i2v based on whether a start_frame was provided.
|
||||
let image_url = optional_url(builder.start_frame.take())?;
|
||||
let end_image_url = optional_url(builder.end_frame.take())?;
|
||||
|
||||
if image_url.is_none() && end_image_url.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "end_frame",
|
||||
value: "Seedance 1.5 Pro requires a start_frame when end_frame is provided".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
|
||||
let resolution = plan_resolution(builder.resolution.take(), strategy)?;
|
||||
let duration = plan_duration(builder.duration_seconds.take(), strategy)?;
|
||||
let prompt = builder.prompt.take().unwrap_or_default();
|
||||
let generate_audio = builder.generate_audio.take();
|
||||
|
||||
let mode = match image_url {
|
||||
None => FalSeedance1p5ProMode::TextToVideo(EnqueueSeedance1p5ProTextToVideoRequest {
|
||||
prompt,
|
||||
resolution: resolution.map(to_t2v_resolution),
|
||||
duration: duration.map(to_t2v_duration),
|
||||
aspect_ratio: aspect_ratio.map(to_t2v_aspect_ratio),
|
||||
generate_audio,
|
||||
}),
|
||||
Some(image_url) => FalSeedance1p5ProMode::ImageToVideo(EnqueueSeedance1p5ProImageToVideoRequest {
|
||||
prompt,
|
||||
image_url,
|
||||
end_image_url,
|
||||
resolution: resolution.map(to_i2v_resolution),
|
||||
duration: duration.map(to_i2v_duration),
|
||||
aspect_ratio: aspect_ratio.map(to_i2v_aspect_ratio),
|
||||
generate_audio,
|
||||
}),
|
||||
};
|
||||
|
||||
let state = FalSeedance1p5ProRequestState { mode };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalSeedance1p5Pro(state)))
|
||||
}
|
||||
|
||||
// ── Plan helpers (kept in sync with v1 plan_generate_video_fal_seedance_1p5_pro.rs) ──
|
||||
|
||||
fn optional_url(image_ref: Option<ImageRef>) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
match image_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageRef::Url(url)) => Ok(Some(url)),
|
||||
Some(ImageRef::MediaFileToken(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "start_frame/end_frame",
|
||||
value: "Fal only supports image URLs, not media file tokens".to_string(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<PlanAspectRatio>, ArtcraftRouterError> {
|
||||
use PlanAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Some(Ar::Auto)),
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(Ar::Square)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(Ar::FourByThree)),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Ar::SixteenByNine)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(Ar::TwentyOneByNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(Ar::ThreeByFour)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Ar::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(nearest_aspect_ratio(unsupported))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick the nearest supported aspect ratio for unsupported inputs.
|
||||
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> PlanAspectRatio {
|
||||
use PlanAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => Ar::FourByThree,
|
||||
CommonAspectRatio::WideThreeByTwo => Ar::FourByThree,
|
||||
CommonAspectRatio::TallFourByFive => Ar::ThreeByFour,
|
||||
CommonAspectRatio::TallTwoByThree => Ar::ThreeByFour,
|
||||
CommonAspectRatio::TallNineByTwentyOne => Ar::NineBySixteen,
|
||||
_ => Ar::Square,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<PlanResolution>, ArtcraftRouterError> {
|
||||
use PlanResolution as R;
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
Some(CommonResolution::FourEightyP) => Ok(Some(R::FourEightyP)),
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(Some(R::SevenTwentyP)),
|
||||
Some(CommonResolution::TenEightyP) => Ok(Some(R::TenEightyP)),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", other),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(R::TenEightyP)),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(R::FourEightyP)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<PlanDuration>, ArtcraftRouterError> {
|
||||
use PlanDuration as D;
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(4) => Ok(Some(D::Four)),
|
||||
Some(5) => Ok(Some(D::Five)),
|
||||
Some(6) => Ok(Some(D::Six)),
|
||||
Some(7) => Ok(Some(D::Seven)),
|
||||
Some(8) => Ok(Some(D::Eight)),
|
||||
Some(9) => Ok(Some(D::Nine)),
|
||||
Some(10) => Ok(Some(D::Ten)),
|
||||
Some(11) => Ok(Some(D::Eleven)),
|
||||
Some(12) => Ok(Some(D::Twelve)),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", other),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(D::Twelve)),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(D::Four)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── PlanXxx → fal_client request enum mapping ──
|
||||
|
||||
fn to_t2v_resolution(r: PlanResolution) -> EnqueueSeedance1p5ProTextToVideoResolution {
|
||||
use EnqueueSeedance1p5ProTextToVideoResolution as R;
|
||||
match r {
|
||||
PlanResolution::FourEightyP => R::FourEightyP,
|
||||
PlanResolution::SevenTwentyP => R::SevenTwentyP,
|
||||
PlanResolution::TenEightyP => R::TenEightyP,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2v_duration(d: PlanDuration) -> EnqueueSeedance1p5ProTextToVideoDuration {
|
||||
use EnqueueSeedance1p5ProTextToVideoDuration as D;
|
||||
match d {
|
||||
PlanDuration::Four => D::FourSeconds,
|
||||
PlanDuration::Five => D::FiveSeconds,
|
||||
PlanDuration::Six => D::SixSeconds,
|
||||
PlanDuration::Seven => D::SevenSeconds,
|
||||
PlanDuration::Eight => D::EightSeconds,
|
||||
PlanDuration::Nine => D::NineSeconds,
|
||||
PlanDuration::Ten => D::TenSeconds,
|
||||
PlanDuration::Eleven => D::ElevenSeconds,
|
||||
PlanDuration::Twelve => D::TwelveSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2v_aspect_ratio(a: PlanAspectRatio) -> EnqueueSeedance1p5ProTextToVideoAspectRatio {
|
||||
use EnqueueSeedance1p5ProTextToVideoAspectRatio as Ar;
|
||||
match a {
|
||||
PlanAspectRatio::Auto => Ar::Auto,
|
||||
PlanAspectRatio::TwentyOneByNine => Ar::TwentyOneByNine,
|
||||
PlanAspectRatio::SixteenByNine => Ar::SixteenByNine,
|
||||
PlanAspectRatio::FourByThree => Ar::FourByThree,
|
||||
PlanAspectRatio::Square => Ar::Square,
|
||||
PlanAspectRatio::ThreeByFour => Ar::ThreeByFour,
|
||||
PlanAspectRatio::NineBySixteen => Ar::NineBySixteen,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_i2v_resolution(r: PlanResolution) -> EnqueueSeedance1p5ProImageToVideoResolution {
|
||||
use EnqueueSeedance1p5ProImageToVideoResolution as R;
|
||||
match r {
|
||||
PlanResolution::FourEightyP => R::FourEightyP,
|
||||
PlanResolution::SevenTwentyP => R::SevenTwentyP,
|
||||
PlanResolution::TenEightyP => R::TenEightyP,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_i2v_duration(d: PlanDuration) -> EnqueueSeedance1p5ProImageToVideoDuration {
|
||||
use EnqueueSeedance1p5ProImageToVideoDuration as D;
|
||||
match d {
|
||||
PlanDuration::Four => D::FourSeconds,
|
||||
PlanDuration::Five => D::FiveSeconds,
|
||||
PlanDuration::Six => D::SixSeconds,
|
||||
PlanDuration::Seven => D::SevenSeconds,
|
||||
PlanDuration::Eight => D::EightSeconds,
|
||||
PlanDuration::Nine => D::NineSeconds,
|
||||
PlanDuration::Ten => D::TenSeconds,
|
||||
PlanDuration::Eleven => D::ElevenSeconds,
|
||||
PlanDuration::Twelve => D::TwelveSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_i2v_aspect_ratio(a: PlanAspectRatio) -> EnqueueSeedance1p5ProImageToVideoAspectRatio {
|
||||
use EnqueueSeedance1p5ProImageToVideoAspectRatio as Ar;
|
||||
match a {
|
||||
PlanAspectRatio::Auto => Ar::Auto,
|
||||
PlanAspectRatio::TwentyOneByNine => Ar::TwentyOneByNine,
|
||||
PlanAspectRatio::SixteenByNine => Ar::SixteenByNine,
|
||||
PlanAspectRatio::FourByThree => Ar::FourByThree,
|
||||
PlanAspectRatio::Square => Ar::Square,
|
||||
PlanAspectRatio::ThreeByFour => Ar::ThreeByFour,
|
||||
PlanAspectRatio::NineBySixteen => Ar::NineBySixteen,
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::webhook::video::text::enqueue_seedance_1p5_pro_text_to_video_webhook::{
|
||||
EnqueueSeedance1p5ProTextToVideoDuration, EnqueueSeedance1p5ProTextToVideoRequest,
|
||||
EnqueueSeedance1p5ProTextToVideoResolution,
|
||||
};
|
||||
use fal_client::requests::webhook::video::image::enqueue_seedance_1p5_pro_image_to_video_webhook::{
|
||||
EnqueueSeedance1p5ProImageToVideoDuration, EnqueueSeedance1p5ProImageToVideoResolution,
|
||||
};
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::fal::seedance_1p5_pro::request::{
|
||||
FalSeedance1p5ProMode, FalSeedance1p5ProRequestState,
|
||||
};
|
||||
|
||||
pub struct FalSeedance1p5ProCostState {
|
||||
request: FalSeedance1p5ProRequestState,
|
||||
}
|
||||
|
||||
impl FalSeedance1p5ProCostState {
|
||||
pub fn from_request(request: &FalSeedance1p5ProRequestState) -> Self {
|
||||
Self { request: request.clone() }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
// Seedance 1.5 Pro t2v and i2v have identical pricing. v1 delegates both
|
||||
// to the t2v calculator; v2 does the same to guarantee billing parity.
|
||||
let t2v_request = match &self.request.mode {
|
||||
FalSeedance1p5ProMode::TextToVideo(req) => req.clone(),
|
||||
FalSeedance1p5ProMode::ImageToVideo(req) => EnqueueSeedance1p5ProTextToVideoRequest {
|
||||
prompt: String::new(),
|
||||
resolution: req.resolution.map(i2v_to_t2v_resolution),
|
||||
duration: req.duration.map(i2v_to_t2v_duration),
|
||||
aspect_ratio: None,
|
||||
generate_audio: req.generate_audio,
|
||||
},
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = t2v_request.calculate_cost_in_cents();
|
||||
|
||||
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,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn i2v_to_t2v_resolution(r: EnqueueSeedance1p5ProImageToVideoResolution) -> EnqueueSeedance1p5ProTextToVideoResolution {
|
||||
use EnqueueSeedance1p5ProImageToVideoResolution as I;
|
||||
use EnqueueSeedance1p5ProTextToVideoResolution as T;
|
||||
match r {
|
||||
I::FourEightyP => T::FourEightyP,
|
||||
I::SevenTwentyP => T::SevenTwentyP,
|
||||
I::TenEightyP => T::TenEightyP,
|
||||
}
|
||||
}
|
||||
|
||||
fn i2v_to_t2v_duration(d: EnqueueSeedance1p5ProImageToVideoDuration) -> EnqueueSeedance1p5ProTextToVideoDuration {
|
||||
use EnqueueSeedance1p5ProImageToVideoDuration as I;
|
||||
use EnqueueSeedance1p5ProTextToVideoDuration as T;
|
||||
match d {
|
||||
I::FourSeconds => T::FourSeconds,
|
||||
I::FiveSeconds => T::FiveSeconds,
|
||||
I::SixSeconds => T::SixSeconds,
|
||||
I::SevenSeconds => T::SevenSeconds,
|
||||
I::EightSeconds => T::EightSeconds,
|
||||
I::NineSeconds => T::NineSeconds,
|
||||
I::TenSeconds => T::TenSeconds,
|
||||
I::ElevenSeconds => T::ElevenSeconds,
|
||||
I::TwelveSeconds => T::TwelveSeconds,
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::webhook::video::image::enqueue_seedance_1p5_pro_image_to_video_webhook::{
|
||||
enqueue_seedance_1p5_pro_image_to_video_webhook, EnqueueSeedance1p5ProImageToVideoArgs,
|
||||
EnqueueSeedance1p5ProImageToVideoRequest,
|
||||
};
|
||||
use fal_client::requests::webhook::video::text::enqueue_seedance_1p5_pro_text_to_video_webhook::{
|
||||
enqueue_seedance_1p5_pro_text_to_video_webhook, EnqueueSeedance1p5ProTextToVideoArgs,
|
||||
EnqueueSeedance1p5ProTextToVideoRequest,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FalSeedance1p5ProMode {
|
||||
TextToVideo(EnqueueSeedance1p5ProTextToVideoRequest),
|
||||
ImageToVideo(EnqueueSeedance1p5ProImageToVideoRequest),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalSeedance1p5ProRequestState {
|
||||
pub mode: FalSeedance1p5ProMode,
|
||||
}
|
||||
|
||||
impl FalSeedance1p5ProRequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let (webhook_response, outbound_request): (_, Arc<dyn Debug + Send + Sync>) = match &self.mode {
|
||||
FalSeedance1p5ProMode::TextToVideo(request) => {
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
|
||||
let args = EnqueueSeedance1p5ProTextToVideoArgs {
|
||||
request: request.clone(),
|
||||
webhook_url: client.webhook_url.as_str(),
|
||||
api_key: &client.api_key,
|
||||
};
|
||||
(enqueue_seedance_1p5_pro_text_to_video_webhook(args).await, outbound)
|
||||
}
|
||||
FalSeedance1p5ProMode::ImageToVideo(request) => {
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
|
||||
let args = EnqueueSeedance1p5ProImageToVideoArgs {
|
||||
request: request.clone(),
|
||||
webhook_url: client.webhook_url.as_str(),
|
||||
api_key: &client.api_key,
|
||||
};
|
||||
(enqueue_seedance_1p5_pro_image_to_video_webhook(args).await, outbound)
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
maybe_outbound_request: Some(outbound_request),
|
||||
}))
|
||||
}
|
||||
}
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
pub mod artcraft;
|
||||
pub mod fal;
|
||||
pub mod gmicloud;
|
||||
pub mod grok_api;
|
||||
pub mod kinovi;
|
||||
+18
@@ -23,6 +23,10 @@ use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp::co
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp::request::ArtcraftSeedance2p0BytePlusRequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp_fast::cost::ArtcraftSeedance2p0BytePlusFastCostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp_fast::request::ArtcraftSeedance2p0BytePlusFastRequestState;
|
||||
use crate::generate::generate_video_v2::providers::fal::seedance_1p0_lite::cost::FalSeedance10LiteCostState;
|
||||
use crate::generate::generate_video_v2::providers::fal::seedance_1p0_lite::request::FalSeedance10LiteRequestState;
|
||||
use crate::generate::generate_video_v2::providers::fal::seedance_1p5_pro::cost::FalSeedance1p5ProCostState;
|
||||
use crate::generate::generate_video_v2::providers::fal::seedance_1p5_pro::request::FalSeedance1p5ProRequestState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::cost::GmiCloudSeedance2p0UltraCostState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0UltraRequestState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::cost::GmiCloudSeedance2p0UltraFastCostState;
|
||||
@@ -48,6 +52,8 @@ pub enum VideoGenerationRequest {
|
||||
ArtcraftSeedance2p0UltraFast(ArtcraftSeedance2p0UltraFastRequestState),
|
||||
ArtcraftSeedance2p0BytePlus(ArtcraftSeedance2p0BytePlusRequestState),
|
||||
ArtcraftSeedance2p0BytePlusFast(ArtcraftSeedance2p0BytePlusFastRequestState),
|
||||
FalSeedance10Lite(FalSeedance10LiteRequestState),
|
||||
FalSeedance1p5Pro(FalSeedance1p5ProRequestState),
|
||||
GmiCloudSeedance2p0Ultra(GmiCloudSeedance2p0UltraRequestState),
|
||||
GmiCloudSeedance2p0UltraFast(GmiCloudSeedance2p0UltraFastRequestState),
|
||||
GrokApiGrokImagineVideo(GrokApiGrokImagineVideoRequestState),
|
||||
@@ -70,6 +76,8 @@ impl VideoGenerationRequest {
|
||||
Self::ArtcraftSeedance2p0UltraFast(_) => Provider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0BytePlus(_) => Provider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0BytePlusFast(_) => Provider::Artcraft,
|
||||
Self::FalSeedance10Lite(_) => Provider::Fal,
|
||||
Self::FalSeedance1p5Pro(_) => Provider::Fal,
|
||||
Self::GmiCloudSeedance2p0Ultra(_) => Provider::GmiCloud,
|
||||
Self::GmiCloudSeedance2p0UltraFast(_) => Provider::GmiCloud,
|
||||
Self::GrokApiGrokImagineVideo(_) => Provider::GrokApi,
|
||||
@@ -92,6 +100,8 @@ impl VideoGenerationRequest {
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0UltraFast(request) => Ok(ArtcraftSeedance2p0UltraFastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlus(request) => Ok(ArtcraftSeedance2p0BytePlusCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusFast(request) => Ok(ArtcraftSeedance2p0BytePlusFastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::FalSeedance10Lite(request) => Ok(FalSeedance10LiteCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::FalSeedance1p5Pro(request) => Ok(FalSeedance1p5ProCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0Ultra(request) => Ok(GmiCloudSeedance2p0UltraCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0UltraFast(request) => Ok(GmiCloudSeedance2p0UltraFastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::GrokApiGrokImagineVideo(request) => Ok(GrokApiGrokImagineVideoCostState::from_request(request).estimate_cost()),
|
||||
@@ -145,6 +155,14 @@ impl VideoGenerationRequest {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::FalSeedance10Lite(request) => {
|
||||
let client_ref = client.get_fal_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::FalSeedance1p5Pro(request) => {
|
||||
let client_ref = client.get_fal_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0Ultra(request) => {
|
||||
let client_ref = client.get_gmicloud_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
|
||||
Reference in New Issue
Block a user