Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec620de195 | ||
|
|
10a7d1fee2 |
No files matched your search
+6
-26
@@ -1,22 +1,22 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
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::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video::providers::veo_3p1_common::{plan_veo_3p1_duration, Veo3p1Modality, Veo3p1Variant};
|
||||
use crate::generate::generate_video::providers::artcraft::veo_3p1::request::ArtcraftVeo3p1RequestState;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_veo_3p1(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
// Veo 3.1 accepts only {4,6,8} seconds; pre-plan so cost parity holds for
|
||||
// strategies that error or clamp. Also preserve generate_audio (omni-gen
|
||||
// helper hardcodes None on output).
|
||||
// Pre-plan the duration fal will actually generate (it depends on the
|
||||
// modality; see `veo_3p1_common`) so the price matches. Also preserve
|
||||
// generate_audio (omni-gen helper hardcodes None on output).
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
let final_duration = plan_veo_3p1_duration(builder.duration_seconds, strategy)?;
|
||||
let modality = Veo3p1Modality::of(&builder);
|
||||
let final_duration = plan_veo_3p1_duration(Veo3p1Variant::Standard, modality, builder.duration_seconds, strategy)?;
|
||||
builder.duration_seconds = final_duration;
|
||||
|
||||
let generate_audio = builder.generate_audio;
|
||||
@@ -31,23 +31,3 @@ pub fn build_artcraft_veo_3p1(mut builder: GenerateVideoRequestBuilder) -> Resul
|
||||
let state = ArtcraftVeo3p1RequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftVeo3p1(state)))
|
||||
}
|
||||
|
||||
pub(crate) fn plan_veo_3p1_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u16>, ArtcraftRouterError> {
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(4) | Some(6) | Some(8) => Ok(duration_seconds),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", other),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(8)),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(4)),
|
||||
},
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -14,7 +14,8 @@ impl ArtcraftVeo3p1CostState {
|
||||
pub fn from_request(request: &ArtcraftVeo3p1RequestState) -> Self {
|
||||
Self {
|
||||
// v1 legacy Veo 3.1 multi-function handler defaults None → 6s.
|
||||
duration_seconds: request.request.duration_seconds.map(u64::from).unwrap_or(6),
|
||||
// NB: fal defaults Veo 3.1 to 8s (the builder plans it explicitly anyway).
|
||||
duration_seconds: request.request.duration_seconds.map(u64::from).unwrap_or(8),
|
||||
// v1 legacy Veo 3.1 handler defaults generate_audio to true.
|
||||
generate_audio: request.request.generate_audio.unwrap_or(true),
|
||||
is_4k: request.request.resolution == Some(CommonResolutionEnum::FourK),
|
||||
@@ -85,8 +86,9 @@ mod tests {
|
||||
fn audio_off_4s_is_96() { assert_eq!(cost_cents(Some(4), Some(false)), 96); }
|
||||
|
||||
#[test]
|
||||
fn default_duration_is_6s() {
|
||||
assert_eq!(cost_cents(None, Some(true)), 288);
|
||||
fn default_duration_is_8s() {
|
||||
// fal defaults Veo 3.1 to 8s.
|
||||
assert_eq!(cost_cents(None, Some(true)), cost_cents(Some(8), Some(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+4
-3
@@ -5,15 +5,16 @@ use crate::generate::generate_video::generate_video_request_builder::GenerateVid
|
||||
use crate::generate::generate_video::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video::providers::artcraft::veo_3p1::build::plan_veo_3p1_duration;
|
||||
use crate::generate::generate_video::providers::veo_3p1_common::{plan_veo_3p1_duration, Veo3p1Modality, Veo3p1Variant};
|
||||
use crate::generate::generate_video::providers::artcraft::veo_3p1_fast::request::ArtcraftVeo3p1FastRequestState;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_veo_3p1_fast(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
// Same duration constraints as Veo 3.1 (4/6/8). See veo_3p1::build for rationale.
|
||||
// Pre-plan the duration fal will actually generate so the price matches.
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
let final_duration = plan_veo_3p1_duration(builder.duration_seconds, strategy)?;
|
||||
let modality = Veo3p1Modality::of(&builder);
|
||||
let final_duration = plan_veo_3p1_duration(Veo3p1Variant::Fast, modality, builder.duration_seconds, strategy)?;
|
||||
builder.duration_seconds = final_duration;
|
||||
|
||||
let generate_audio = builder.generate_audio;
|
||||
|
||||
+5
-3
@@ -14,7 +14,8 @@ impl ArtcraftVeo3p1FastCostState {
|
||||
pub fn from_request(request: &ArtcraftVeo3p1FastRequestState) -> Self {
|
||||
Self {
|
||||
// v1 legacy Veo 3.1 Fast multi-function handler defaults None → 6s.
|
||||
duration_seconds: request.request.duration_seconds.map(u64::from).unwrap_or(6),
|
||||
// NB: fal defaults Veo 3.1 to 8s (the builder plans it explicitly anyway).
|
||||
duration_seconds: request.request.duration_seconds.map(u64::from).unwrap_or(8),
|
||||
// v1 legacy Veo 3.1 Fast handler defaults generate_audio to true.
|
||||
generate_audio: request.request.generate_audio.unwrap_or(true),
|
||||
is_4k: request.request.resolution == Some(CommonResolutionEnum::FourK),
|
||||
@@ -85,8 +86,9 @@ mod tests {
|
||||
fn audio_off_4s_is_44() { assert_eq!(cost_cents(Some(4), Some(false)), 44); }
|
||||
|
||||
#[test]
|
||||
fn default_duration_is_6s() {
|
||||
assert_eq!(cost_cents(None, Some(true)), 99);
|
||||
fn default_duration_is_8s() {
|
||||
// fal defaults Veo 3.1 to 8s.
|
||||
assert_eq!(cost_cents(None, Some(true)), cost_cents(Some(8), Some(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+8
-1
@@ -5,6 +5,7 @@ use crate::generate::generate_video::generate_video_request_builder::GenerateVid
|
||||
use crate::generate::generate_video::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video::providers::veo_3p1_common::{plan_veo_3p1_duration, Veo3p1Modality, Veo3p1Variant};
|
||||
use crate::generate::generate_video::providers::artcraft::veo_3p1_lite::request::ArtcraftVeo3p1LiteRequestState;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
@@ -14,7 +15,13 @@ pub fn build_artcraft_veo_3p1_lite(builder: GenerateVideoRequestBuilder) -> Resu
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftVeo3p1Lite(state)))
|
||||
}
|
||||
|
||||
pub(crate) fn build_artcraft_veo_3p1_lite_state(builder: GenerateVideoRequestBuilder) -> Result<ArtcraftVeo3p1LiteRequestState, ArtcraftRouterError> {
|
||||
pub(crate) fn build_artcraft_veo_3p1_lite_state(mut builder: GenerateVideoRequestBuilder) -> Result<ArtcraftVeo3p1LiteRequestState, ArtcraftRouterError> {
|
||||
// Pre-plan the duration fal will actually generate (it depends on the
|
||||
// modality; see `veo_3p1_common`) so the price matches.
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
let modality = Veo3p1Modality::of(&builder);
|
||||
builder.duration_seconds = plan_veo_3p1_duration(Veo3p1Variant::Lite, modality, builder.duration_seconds, strategy)?;
|
||||
|
||||
// Veo 3.1 Lite supports `generate_audio` — preserve it so cost can read it back.
|
||||
// build_artcraft_omni_video_request hardcodes generate_audio = None on its output.
|
||||
let generate_audio = builder.generate_audio;
|
||||
|
||||
+28
-46
@@ -19,6 +19,7 @@ use fal_client::requests::api::video::text::veo_3p1::api::{
|
||||
Veo3p1TextToVideoResolution,
|
||||
};
|
||||
|
||||
use crate::generate::generate_video::providers::veo_3p1_common::{plan_veo_3p1_duration, Veo3p1Modality, Veo3p1Variant};
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
@@ -139,14 +140,19 @@ pub(crate) fn build_fal_veo_3p1_state(
|
||||
prompt,
|
||||
image_urls,
|
||||
aspect_ratio: aspect_ratio.and_then(to_reference_aspect_ratio),
|
||||
duration: plan_duration(builder.duration_seconds, strategy)?.map(to_reference_duration),
|
||||
duration: plan_duration(builder.duration_seconds, Veo3p1Modality::ReferenceToVideo, strategy)?.map(to_reference_duration),
|
||||
resolution: resolution.map(to_reference_resolution),
|
||||
generate_audio,
|
||||
auto_fix: None,
|
||||
safety_tolerance: None,
|
||||
})
|
||||
} else {
|
||||
let duration = plan_duration(builder.duration_seconds, strategy)?;
|
||||
let modality = match (&start, &end) {
|
||||
(Some(_), Some(_)) => Veo3p1Modality::FirstLastFrameToVideo,
|
||||
(Some(_), None) => Veo3p1Modality::ImageToVideo,
|
||||
(None, _) => Veo3p1Modality::TextToVideo,
|
||||
};
|
||||
let duration = plan_duration(builder.duration_seconds, modality, strategy)?;
|
||||
match (start, end) {
|
||||
(None, None) => FalVeo3p1Mode::TextToVideo(Veo3p1TextToVideoRequest {
|
||||
prompt,
|
||||
@@ -276,57 +282,31 @@ fn plan_resolution(
|
||||
}
|
||||
}
|
||||
|
||||
/// fal's accepted durations depend on the modality; see `veo_3p1_common`.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
modality: Veo3p1Modality,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<PlanDuration>, ArtcraftRouterError> {
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(4) => Ok(Some(PlanDuration::Four)),
|
||||
Some(6) => Ok(Some(PlanDuration::Six)),
|
||||
Some(8) => Ok(Some(PlanDuration::Eight)),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(unsupported("duration_seconds", &format!("{}", other)))
|
||||
}
|
||||
// Nearest supported duration above/below, clamped to the 4s–8s range.
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(match other {
|
||||
0..=4 => PlanDuration::Four,
|
||||
5..=6 => PlanDuration::Six,
|
||||
_ => PlanDuration::Eight,
|
||||
})),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(match other {
|
||||
0..=5 => PlanDuration::Four,
|
||||
6..=7 => PlanDuration::Six,
|
||||
_ => PlanDuration::Eight,
|
||||
})),
|
||||
},
|
||||
}
|
||||
plan_veo_3p1_duration(Veo3p1Variant::Standard, modality, duration_seconds, strategy)?
|
||||
.map(|seconds| match seconds {
|
||||
4 => Ok(PlanDuration::Four),
|
||||
6 => Ok(PlanDuration::Six),
|
||||
8 => Ok(PlanDuration::Eight),
|
||||
other => Err(unsupported("duration_seconds", &format!("{}", other))),
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// Extend-video also supports fal's 7s default in addition to 4/6/8.
|
||||
/// fal's extend-video endpoints only accept 7s; see `veo_3p1_common`.
|
||||
fn plan_extend_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Veo3p1ExtendVideoDuration>, ArtcraftRouterError> {
|
||||
use Veo3p1ExtendVideoDuration as D;
|
||||
match duration_seconds {
|
||||
match plan_veo_3p1_duration(Veo3p1Variant::Standard, Veo3p1Modality::ExtendVideo, duration_seconds, strategy)? {
|
||||
None => Ok(None),
|
||||
Some(4) => Ok(Some(D::FourSeconds)),
|
||||
Some(6) => Ok(Some(D::SixSeconds)),
|
||||
Some(7) => Ok(Some(D::SevenSeconds)),
|
||||
Some(8) => Ok(Some(D::EightSeconds)),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(unsupported("duration_seconds", &format!("{}", other)))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
Ok(Some(if other < 5 { D::FourSeconds } else if other == 5 { D::SixSeconds } else { D::EightSeconds }))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(if other <= 5 { D::FourSeconds } else { D::EightSeconds }))
|
||||
}
|
||||
},
|
||||
Some(7) => Ok(Some(Veo3p1ExtendVideoDuration::SevenSeconds)),
|
||||
Some(other) => Err(unsupported("duration_seconds", &format!("{}", other))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,14 +572,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_duration_5s_pay_more_upgrades_to_6() {
|
||||
fn extend_duration_is_always_7s() {
|
||||
// fal's extend-video endpoint only accepts "7s".
|
||||
let mut b = base_builder();
|
||||
b.reference_videos = Some(VideoListRef::Urls(vec![REFERENCE_VIDEO_URL.to_string()]));
|
||||
b.duration_seconds = Some(5);
|
||||
b.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::PayMoreUpgrade;
|
||||
match unwrap_mode(b) {
|
||||
FalVeo3p1Mode::ExtendVideo(r) => {
|
||||
assert!(matches!(r.duration, Some(Veo3p1ExtendVideoDuration::SixSeconds)));
|
||||
assert!(matches!(r.duration, Some(Veo3p1ExtendVideoDuration::SevenSeconds)));
|
||||
}
|
||||
_ => panic!("expected extend"),
|
||||
}
|
||||
@@ -681,14 +662,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_maps_duration_and_resolution() {
|
||||
fn reference_duration_is_always_8s_and_maps_resolution() {
|
||||
// fal's reference-to-video endpoint only accepts "8s" (it rejected 6s in production).
|
||||
let mut b = base_builder();
|
||||
b.reference_images = Some(ImageListRef::Urls(vec![REFERENCE_IMAGE_URL_A.to_string()]));
|
||||
b.duration_seconds = Some(6);
|
||||
b.resolution = Some(RouterResolution::FourK);
|
||||
match unwrap_mode(b) {
|
||||
FalVeo3p1Mode::ReferenceToVideo(r) => {
|
||||
assert!(matches!(r.duration, Some(Veo3p1ReferenceToVideoDuration::SixSeconds)));
|
||||
assert!(matches!(r.duration, Some(Veo3p1ReferenceToVideoDuration::EightSeconds)));
|
||||
assert!(matches!(r.resolution, Some(Veo3p1ReferenceToVideoResolution::FourK)));
|
||||
}
|
||||
_ => panic!("expected reference"),
|
||||
|
||||
+7
-6
@@ -148,8 +148,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_4s_audio_off_720p_is_80() {
|
||||
assert_eq!(cost_cents(Modality::Reference, Some(4), Some(RouterResolution::SevenTwentyP), Some(false)), 80);
|
||||
fn reference_4s_request_is_billed_as_8s_audio_off_720p_is_160() {
|
||||
assert_eq!(cost_cents(Modality::Reference, Some(4), Some(RouterResolution::SevenTwentyP), Some(false)), 160);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -172,8 +172,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_8s_audio_on_is_320() {
|
||||
assert_eq!(cost_cents(Modality::Extend, Some(8), None, Some(true)), 320);
|
||||
fn extend_8s_request_is_billed_as_7s_audio_on_is_280() {
|
||||
assert_eq!(cost_cents(Modality::Extend, Some(8), None, Some(true)), 280);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -185,9 +185,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn t2v_i2v_flf_and_reference_price_identically() {
|
||||
let expected = cost_cents(Modality::TextToVideo, Some(6), Some(RouterResolution::TenEightyP), Some(true));
|
||||
// NB: At 8s, the only duration reference-to-video accepts.
|
||||
let expected = cost_cents(Modality::TextToVideo, Some(8), Some(RouterResolution::TenEightyP), Some(true));
|
||||
for modality in [Modality::ImageToVideo, Modality::FirstLastFrame, Modality::Reference] {
|
||||
assert_eq!(cost_cents(modality, Some(6), Some(RouterResolution::TenEightyP), Some(true)), expected);
|
||||
assert_eq!(cost_cents(modality, Some(8), Some(RouterResolution::TenEightyP), Some(true)), expected);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-42
@@ -19,6 +19,7 @@ use fal_client::requests::api::video::text::veo_3p1_fast::api::{
|
||||
Veo3p1FastTextToVideoResolution,
|
||||
};
|
||||
|
||||
use crate::generate::generate_video::providers::veo_3p1_common::{plan_veo_3p1_duration, Veo3p1Modality, Veo3p1Variant};
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
@@ -141,14 +142,19 @@ pub(crate) fn build_fal_veo_3p1_fast_state(
|
||||
prompt,
|
||||
image_urls,
|
||||
aspect_ratio: aspect_ratio.and_then(to_reference_aspect_ratio),
|
||||
duration: plan_duration(builder.duration_seconds, strategy)?.map(to_reference_duration),
|
||||
duration: plan_duration(builder.duration_seconds, Veo3p1Modality::ReferenceToVideo, strategy)?.map(to_reference_duration),
|
||||
resolution: resolution.map(to_reference_resolution),
|
||||
generate_audio,
|
||||
auto_fix: None,
|
||||
safety_tolerance: None,
|
||||
})
|
||||
} else {
|
||||
let duration = plan_duration(builder.duration_seconds, strategy)?;
|
||||
let modality = match (&start, &end) {
|
||||
(Some(_), Some(_)) => Veo3p1Modality::FirstLastFrameToVideo,
|
||||
(Some(_), None) => Veo3p1Modality::ImageToVideo,
|
||||
(None, _) => Veo3p1Modality::TextToVideo,
|
||||
};
|
||||
let duration = plan_duration(builder.duration_seconds, modality, strategy)?;
|
||||
match (start, end) {
|
||||
(None, None) => FalVeo3p1FastMode::TextToVideo(Veo3p1FastTextToVideoRequest {
|
||||
prompt,
|
||||
@@ -278,57 +284,31 @@ fn plan_resolution(
|
||||
}
|
||||
}
|
||||
|
||||
/// fal's accepted durations depend on the modality; see `veo_3p1_common`.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
modality: Veo3p1Modality,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<PlanDuration>, ArtcraftRouterError> {
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(4) => Ok(Some(PlanDuration::Four)),
|
||||
Some(6) => Ok(Some(PlanDuration::Six)),
|
||||
Some(8) => Ok(Some(PlanDuration::Eight)),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(unsupported("duration_seconds", &format!("{}", other)))
|
||||
}
|
||||
// Nearest supported duration above/below, clamped to the 4s–8s range.
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(match other {
|
||||
0..=4 => PlanDuration::Four,
|
||||
5..=6 => PlanDuration::Six,
|
||||
_ => PlanDuration::Eight,
|
||||
})),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(match other {
|
||||
0..=5 => PlanDuration::Four,
|
||||
6..=7 => PlanDuration::Six,
|
||||
_ => PlanDuration::Eight,
|
||||
})),
|
||||
},
|
||||
}
|
||||
plan_veo_3p1_duration(Veo3p1Variant::Fast, modality, duration_seconds, strategy)?
|
||||
.map(|seconds| match seconds {
|
||||
4 => Ok(PlanDuration::Four),
|
||||
6 => Ok(PlanDuration::Six),
|
||||
8 => Ok(PlanDuration::Eight),
|
||||
other => Err(unsupported("duration_seconds", &format!("{}", other))),
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// Extend-video also supports fal's 7s default in addition to 4/6/8.
|
||||
/// fal's extend-video endpoints only accept 7s; see `veo_3p1_common`.
|
||||
fn plan_extend_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Veo3p1FastExtendVideoDuration>, ArtcraftRouterError> {
|
||||
use Veo3p1FastExtendVideoDuration as D;
|
||||
match duration_seconds {
|
||||
match plan_veo_3p1_duration(Veo3p1Variant::Fast, Veo3p1Modality::ExtendVideo, duration_seconds, strategy)? {
|
||||
None => Ok(None),
|
||||
Some(4) => Ok(Some(D::FourSeconds)),
|
||||
Some(6) => Ok(Some(D::SixSeconds)),
|
||||
Some(7) => Ok(Some(D::SevenSeconds)),
|
||||
Some(8) => Ok(Some(D::EightSeconds)),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(unsupported("duration_seconds", &format!("{}", other)))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
Ok(Some(if other < 5 { D::FourSeconds } else if other == 5 { D::SixSeconds } else { D::EightSeconds }))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(if other <= 5 { D::FourSeconds } else { D::EightSeconds }))
|
||||
}
|
||||
},
|
||||
Some(7) => Ok(Some(Veo3p1FastExtendVideoDuration::SevenSeconds)),
|
||||
Some(other) => Err(unsupported("duration_seconds", &format!("{}", other))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-6
@@ -155,8 +155,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_4s_audio_off_720p_is_40() {
|
||||
assert_eq!(cost_cents(Modality::Reference, Some(4), Some(RouterResolution::SevenTwentyP), Some(false)), 40);
|
||||
fn reference_4s_request_is_billed_as_8s_audio_off_720p_is_80() {
|
||||
assert_eq!(cost_cents(Modality::Reference, Some(4), Some(RouterResolution::SevenTwentyP), Some(false)), 80);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,8 +174,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_8s_audio_on_is_120() {
|
||||
assert_eq!(cost_cents(Modality::Extend, Some(8), None, Some(true)), 120);
|
||||
fn extend_8s_request_is_billed_as_7s_audio_on_is_105() {
|
||||
assert_eq!(cost_cents(Modality::Extend, Some(8), None, Some(true)), 105);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -187,9 +187,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn t2v_i2v_flf_and_reference_price_identically() {
|
||||
let expected = cost_cents(Modality::TextToVideo, Some(6), Some(RouterResolution::TenEightyP), Some(true));
|
||||
// NB: At 8s, the only duration reference-to-video accepts.
|
||||
let expected = cost_cents(Modality::TextToVideo, Some(8), Some(RouterResolution::TenEightyP), Some(true));
|
||||
for modality in [Modality::ImageToVideo, Modality::FirstLastFrame, Modality::Reference] {
|
||||
assert_eq!(cost_cents(modality, Some(6), Some(RouterResolution::TenEightyP), Some(true)), expected);
|
||||
assert_eq!(cost_cents(modality, Some(8), Some(RouterResolution::TenEightyP), Some(true)), expected);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-22
@@ -11,6 +11,7 @@ use fal_client::requests::api::video::text::veo_3p1_lite::api::{
|
||||
Veo3p1LiteTextToVideoResolution,
|
||||
};
|
||||
|
||||
use crate::generate::generate_video::providers::veo_3p1_common::{plan_veo_3p1_duration, Veo3p1Modality, Veo3p1Variant};
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
@@ -77,11 +78,17 @@ pub(crate) fn build_fal_veo_3p1_lite_state(
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio, strategy)?;
|
||||
let resolution = plan_resolution(builder.resolution, strategy)?;
|
||||
let duration = plan_duration(builder.duration_seconds, strategy)?;
|
||||
let prompt = builder.prompt.clone().unwrap_or_default();
|
||||
let negative_prompt = builder.negative_prompt.clone();
|
||||
let generate_audio = builder.generate_audio;
|
||||
|
||||
let modality = match (&start, &end) {
|
||||
(Some(_), Some(_)) => Veo3p1Modality::FirstLastFrameToVideo,
|
||||
(Some(_), None) => Veo3p1Modality::ImageToVideo,
|
||||
(None, _) => Veo3p1Modality::TextToVideo,
|
||||
};
|
||||
let duration = plan_duration(builder.duration_seconds, modality, strategy)?;
|
||||
|
||||
let mode = match (start, end) {
|
||||
(None, None) => FalVeo3p1LiteMode::TextToVideo(Veo3p1LiteTextToVideoRequest {
|
||||
prompt,
|
||||
@@ -209,31 +216,20 @@ fn plan_resolution(
|
||||
}
|
||||
}
|
||||
|
||||
/// fal's accepted durations depend on the modality; see `veo_3p1_common`.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
modality: Veo3p1Modality,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<PlanDuration>, ArtcraftRouterError> {
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(4) => Ok(Some(PlanDuration::Four)),
|
||||
Some(6) => Ok(Some(PlanDuration::Six)),
|
||||
Some(8) => Ok(Some(PlanDuration::Eight)),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(unsupported("duration_seconds", &format!("{}", other)))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(match other {
|
||||
0..=4 => PlanDuration::Four,
|
||||
5..=6 => PlanDuration::Six,
|
||||
_ => PlanDuration::Eight,
|
||||
})),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(match other {
|
||||
0..=5 => PlanDuration::Four,
|
||||
6..=7 => PlanDuration::Six,
|
||||
_ => PlanDuration::Eight,
|
||||
})),
|
||||
},
|
||||
}
|
||||
plan_veo_3p1_duration(Veo3p1Variant::Lite, modality, duration_seconds, strategy)?
|
||||
.map(|seconds| match seconds {
|
||||
4 => Ok(PlanDuration::Four),
|
||||
6 => Ok(PlanDuration::Six),
|
||||
8 => Ok(PlanDuration::Eight),
|
||||
other => Err(unsupported("duration_seconds", &format!("{}", other))),
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn unsupported(field: &'static str, value: &str) -> ArtcraftRouterError {
|
||||
|
||||
+7
-5
@@ -114,16 +114,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flf_4s_audio_off_720p_is_12() {
|
||||
assert_eq!(cost_cents(Some(4), Some(RouterResolution::SevenTwentyP), Some(false), 2), 12);
|
||||
fn flf_4s_request_is_billed_as_8s_audio_off_720p_is_24() {
|
||||
// Lite's first-last-frame endpoint only accepts "8s".
|
||||
assert_eq!(cost_cents(Some(4), Some(RouterResolution::SevenTwentyP), Some(false), 2), 24);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_three_modes_price_identically() {
|
||||
let t2v = cost_cents(Some(6), Some(RouterResolution::TenEightyP), Some(true), 0);
|
||||
let i2v = cost_cents(Some(6), Some(RouterResolution::TenEightyP), Some(true), 1);
|
||||
let flf = cost_cents(Some(6), Some(RouterResolution::TenEightyP), Some(true), 2);
|
||||
// NB: At 8s, the only duration first-last-frame accepts.
|
||||
let t2v = cost_cents(Some(8), Some(RouterResolution::TenEightyP), Some(true), 0);
|
||||
let i2v = cost_cents(Some(8), Some(RouterResolution::TenEightyP), Some(true), 1);
|
||||
let flf = cost_cents(Some(8), Some(RouterResolution::TenEightyP), Some(true), 2);
|
||||
assert_eq!(t2v, i2v);
|
||||
assert_eq!(i2v, flf);
|
||||
}
|
||||
|
||||
+1
@@ -3,4 +3,5 @@ pub mod fal;
|
||||
pub mod first_party;
|
||||
pub mod grok_api;
|
||||
pub mod kinovi;
|
||||
pub mod veo_3p1_common;
|
||||
pub mod wan_3p0_common;
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
//! Veo 3.1 duration rules, shared by the Artcraft provider (which prices a
|
||||
//! request) and the fal provider (which executes it) so both always agree on
|
||||
//! the duration that will actually be generated and billed.
|
||||
//!
|
||||
//! fal's Veo 3.1 endpoints don't all accept the same durations. From fal's
|
||||
//! OpenAPI schemas (checked 2026-09-28):
|
||||
//!
|
||||
//! | Modality | Veo 3.1 / 3.1 Fast | Veo 3.1 Lite |
|
||||
//! |---------------------------|--------------------|--------------|
|
||||
//! | text-to-video | 4s, 6s, 8s | 4s, 6s, 8s |
|
||||
//! | image-to-video | 4s, 6s, 8s | 4s, 6s, 8s |
|
||||
//! | first-last-frame-to-video | 4s, 6s, 8s | 8s only |
|
||||
//! | reference-to-video | 8s only | (none) |
|
||||
//! | extend-video | 7s only | (none) |
|
||||
//!
|
||||
//! Sending another value fails at fal, eg. `Input should be '8s'`.
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
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;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Veo3p1Variant {
|
||||
Standard,
|
||||
Fast,
|
||||
Lite,
|
||||
}
|
||||
|
||||
/// The fal endpoint a request goes to. Mirrors the fal builders' dispatch
|
||||
/// order: reference videos, then reference images, then frames.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Veo3p1Modality {
|
||||
TextToVideo,
|
||||
ImageToVideo,
|
||||
FirstLastFrameToVideo,
|
||||
ReferenceToVideo,
|
||||
ExtendVideo,
|
||||
}
|
||||
|
||||
impl Veo3p1Modality {
|
||||
pub(crate) fn of(builder: &GenerateVideoRequestBuilder) -> Self {
|
||||
if has_videos(&builder.reference_videos) {
|
||||
Veo3p1Modality::ExtendVideo
|
||||
} else if has_images(&builder.reference_images) {
|
||||
Veo3p1Modality::ReferenceToVideo
|
||||
} else {
|
||||
match (builder.start_frame.is_some(), builder.end_frame.is_some()) {
|
||||
(true, true) => Veo3p1Modality::FirstLastFrameToVideo,
|
||||
(true, false) => Veo3p1Modality::ImageToVideo,
|
||||
// (false, true) is rejected by the fal builders.
|
||||
(false, _) => Veo3p1Modality::TextToVideo,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The durations (seconds, ascending) fal accepts for this variant and modality.
|
||||
pub(crate) fn veo_3p1_allowed_durations(variant: Veo3p1Variant, modality: Veo3p1Modality) -> &'static [u16] {
|
||||
match (variant, modality) {
|
||||
(_, Veo3p1Modality::ExtendVideo) => &[7],
|
||||
(_, Veo3p1Modality::ReferenceToVideo) => &[8],
|
||||
(Veo3p1Variant::Lite, Veo3p1Modality::FirstLastFrameToVideo) => &[8],
|
||||
_ => &[4, 6, 8],
|
||||
}
|
||||
}
|
||||
|
||||
/// Plan the duration to send for a request.
|
||||
///
|
||||
/// A supported duration passes through. Otherwise `ErrorOut` fails, and the
|
||||
/// other strategies pick the nearest supported duration above
|
||||
/// (`PayMoreUpgrade`) or below (`PayLessDowngrade`), clamped to the range.
|
||||
/// With no requested duration, returns fal's default for the endpoint (the
|
||||
/// longest allowed: 8s, or 7s for extend-video), explicitly, so the price is
|
||||
/// computed from the duration that will actually be generated.
|
||||
pub(crate) fn plan_veo_3p1_duration(
|
||||
variant: Veo3p1Variant,
|
||||
modality: Veo3p1Modality,
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u16>, ArtcraftRouterError> {
|
||||
let allowed = veo_3p1_allowed_durations(variant, modality);
|
||||
let shortest = allowed[0];
|
||||
let longest = allowed[allowed.len() - 1];
|
||||
let requested = match duration_seconds {
|
||||
None => return Ok(Some(longest)),
|
||||
Some(requested) => requested,
|
||||
};
|
||||
if allowed.contains(&requested) {
|
||||
return Ok(Some(requested));
|
||||
}
|
||||
match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{} (supported for this input: {:?})", requested, allowed),
|
||||
})),
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(
|
||||
allowed.iter().copied().find(|&allowed| allowed >= requested).unwrap_or(longest))),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(
|
||||
allowed.iter().copied().rev().find(|&allowed| allowed <= requested).unwrap_or(shortest))),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_images(refs: &Option<ImageListRef>) -> bool {
|
||||
match refs {
|
||||
None => false,
|
||||
Some(ImageListRef::Urls(urls)) => !urls.is_empty(),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => !tokens.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_videos(refs: &Option<VideoListRef>) -> bool {
|
||||
match refs {
|
||||
None => false,
|
||||
Some(VideoListRef::Urls(urls)) => !urls.is_empty(),
|
||||
Some(VideoListRef::MediaFileTokens(tokens)) => !tokens.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use RequestMismatchMitigationStrategy::{ErrorOut, PayLessDowngrade, PayMoreUpgrade};
|
||||
use Veo3p1Modality::*;
|
||||
use Veo3p1Variant::*;
|
||||
|
||||
mod allowed_durations {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn standard_and_fast() {
|
||||
for variant in [Standard, Fast] {
|
||||
assert_eq!(veo_3p1_allowed_durations(variant, TextToVideo), &[4, 6, 8]);
|
||||
assert_eq!(veo_3p1_allowed_durations(variant, ImageToVideo), &[4, 6, 8]);
|
||||
assert_eq!(veo_3p1_allowed_durations(variant, FirstLastFrameToVideo), &[4, 6, 8]);
|
||||
assert_eq!(veo_3p1_allowed_durations(variant, ReferenceToVideo), &[8]);
|
||||
assert_eq!(veo_3p1_allowed_durations(variant, ExtendVideo), &[7]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lite() {
|
||||
assert_eq!(veo_3p1_allowed_durations(Lite, TextToVideo), &[4, 6, 8]);
|
||||
assert_eq!(veo_3p1_allowed_durations(Lite, ImageToVideo), &[4, 6, 8]);
|
||||
assert_eq!(veo_3p1_allowed_durations(Lite, FirstLastFrameToVideo), &[8]);
|
||||
}
|
||||
}
|
||||
|
||||
mod planning {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn supported_durations_pass_through() {
|
||||
for seconds in [4, 6, 8] {
|
||||
assert_eq!(plan_veo_3p1_duration(Fast, TextToVideo, Some(seconds), ErrorOut).unwrap(), Some(seconds));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_to_video_is_always_eight_seconds() {
|
||||
// The production bug: 4s/6s sent to reference-to-video ("Input should be '8s'").
|
||||
for seconds in [4, 6] {
|
||||
assert_eq!(plan_veo_3p1_duration(Fast, ReferenceToVideo, Some(seconds), PayMoreUpgrade).unwrap(), Some(8));
|
||||
assert_eq!(plan_veo_3p1_duration(Fast, ReferenceToVideo, Some(seconds), PayLessDowngrade).unwrap(), Some(8));
|
||||
assert!(plan_veo_3p1_duration(Fast, ReferenceToVideo, Some(seconds), ErrorOut).is_err());
|
||||
}
|
||||
assert_eq!(plan_veo_3p1_duration(Fast, ReferenceToVideo, None, ErrorOut).unwrap(), Some(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_video_is_always_seven_seconds() {
|
||||
assert_eq!(plan_veo_3p1_duration(Standard, ExtendVideo, Some(8), PayMoreUpgrade).unwrap(), Some(7));
|
||||
assert_eq!(plan_veo_3p1_duration(Standard, ExtendVideo, Some(4), PayMoreUpgrade).unwrap(), Some(7));
|
||||
assert_eq!(plan_veo_3p1_duration(Standard, ExtendVideo, None, ErrorOut).unwrap(), Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lite_first_last_frame_is_always_eight_seconds() {
|
||||
assert_eq!(plan_veo_3p1_duration(Lite, FirstLastFrameToVideo, Some(4), PayMoreUpgrade).unwrap(), Some(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_durations_round_to_the_nearest_supported_one() {
|
||||
assert_eq!(plan_veo_3p1_duration(Fast, TextToVideo, Some(5), PayMoreUpgrade).unwrap(), Some(6));
|
||||
assert_eq!(plan_veo_3p1_duration(Fast, TextToVideo, Some(7), PayMoreUpgrade).unwrap(), Some(8));
|
||||
assert_eq!(plan_veo_3p1_duration(Fast, TextToVideo, Some(12), PayMoreUpgrade).unwrap(), Some(8));
|
||||
assert_eq!(plan_veo_3p1_duration(Fast, TextToVideo, Some(5), PayLessDowngrade).unwrap(), Some(4));
|
||||
assert_eq!(plan_veo_3p1_duration(Fast, TextToVideo, Some(2), PayLessDowngrade).unwrap(), Some(4));
|
||||
assert!(plan_veo_3p1_duration(Fast, TextToVideo, Some(5), ErrorOut).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_duration_is_fals_default() {
|
||||
// fal defaults every Veo 3.1 endpoint to 8s, except extend-video (7s).
|
||||
for modality in [TextToVideo, ImageToVideo, FirstLastFrameToVideo, ReferenceToVideo] {
|
||||
assert_eq!(plan_veo_3p1_duration(Standard, modality, None, ErrorOut).unwrap(), Some(8), "{:?}", modality);
|
||||
}
|
||||
assert_eq!(plan_veo_3p1_duration(Standard, ExtendVideo, None, ErrorOut).unwrap(), Some(7));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,19 +37,6 @@ fn build_omni_gen_mesh_models() -> Vec<OmniGenMeshModelDetails> {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Hunyuan 3D 2.1: image-to-3D only (exactly one input image).
|
||||
models.push(OmniGenMeshModelDetails {
|
||||
model: CommonMeshModel::Hunyuan3d2p1,
|
||||
model_creator: Some(ModelCreator::Tencent),
|
||||
full_name: Some("Hunyuan 3D 2.1".to_string()),
|
||||
image_input_supported: Some(true),
|
||||
mesh_output_types: Some(vec![
|
||||
CommonMeshOutputType::Normal,
|
||||
CommonMeshOutputType::Geometry,
|
||||
]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Hunyuan 3D 3: text and/or image input with multi-view support and full
|
||||
// output shaping controls (output type, polygon type, face count, PBR).
|
||||
models.push(OmniGenMeshModelDetails {
|
||||
@@ -224,10 +211,6 @@ fn build_omni_gen_mesh_model_providers() -> Vec<OmniGenMeshModelProviderDetails>
|
||||
model: CommonMeshModel::Hunyuan3d2p0,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenMeshProviderModelDetails {
|
||||
model: CommonMeshModel::Hunyuan3d2p1,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenMeshProviderModelDetails {
|
||||
model: CommonMeshModel::Hunyuan3d3,
|
||||
overrides: None,
|
||||
@@ -269,3 +252,22 @@ fn build_omni_gen_mesh_model_providers() -> Vec<OmniGenMeshModelProviderDetails>
|
||||
|
||||
providers
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::http_server::endpoints::omni_gen::shared_utils::mesh::resolve_mesh_model::{mesh_model_status, MeshModelStatus};
|
||||
|
||||
/// Replaced models (see `resolve_mesh_model`) must not be offered.
|
||||
#[test]
|
||||
fn only_available_models_are_listed() {
|
||||
for details in build_omni_gen_mesh_models() {
|
||||
assert_eq!(mesh_model_status(details.model), MeshModelStatus::Available, "{:?}", details.model);
|
||||
}
|
||||
for provider in build_omni_gen_mesh_model_providers() {
|
||||
for details in provider.models {
|
||||
assert_eq!(mesh_model_status(details.model), MeshModelStatus::Available, "{:?}", details.model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-73
@@ -7,26 +7,6 @@ use enums::common::generation::model_creator::ModelCreator;
|
||||
pub fn kling_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Kling16Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 1.6 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true), // NB: 1.6 Elements!
|
||||
image_references_max: Some(4), // NB: 1.6 Elements!
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// TODO: Kling 2.5 doesn't let you control aspect ratio for image-to-video
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Kling2p5TurboPro,
|
||||
@@ -72,59 +52,6 @@ pub fn kling_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
pub fn kling_disabled_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling16Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 1.6 Pro".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling21Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.1 Pro".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling21Master,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.1 Master".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling2p5TurboPro,
|
||||
|
||||
@@ -6,7 +6,6 @@ pub mod minimax_video_models;
|
||||
pub mod seedance_1x_video_models;
|
||||
pub mod seedance_2p0_video_models;
|
||||
pub mod seedance_2p5_video_models;
|
||||
pub mod sora_video_models;
|
||||
pub mod veo_video_models;
|
||||
pub mod vidu_video_models;
|
||||
pub mod wan_video_models;
|
||||
-28
@@ -45,33 +45,5 @@ pub fn seedance_1p5_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
pub fn seedance_1p0_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Seedance10Lite,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 1.0 Lite".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
-46
@@ -111,52 +111,6 @@ pub fn seedance_2p5_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p5Preview,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.5 Preview".to_string()),
|
||||
extra_info: Some("A preview of Seedance 2.5. Supports videos up to 30 seconds and up to 30 reference images, but only reference mode (no start/end keyframes) and 480p/720p output.".to_string()),
|
||||
extra_info_short: Some("Seedance 2.5 preview; up to 30s videos".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
text_prompt_max_length: Some(10_000),
|
||||
starting_keyframe_supported: Some(false),
|
||||
ending_keyframe_supported: Some(false),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(30),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(10),
|
||||
audio_references_max_total_duration_seconds: Some(30),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(10),
|
||||
video_references_max_total_duration_seconds: Some(30),
|
||||
character_references_supported: Some(false),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(30),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
use artcraft_api_defs::omni_gen::models::omni_gen_video_models::OmniGenVideoModelDetails;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
|
||||
/// Sora (OpenAI) video models.
|
||||
pub fn sora_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Sora2,
|
||||
model_creator: Some(ModelCreator::OpenAi),
|
||||
full_name: Some("Sora 2".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![CommonResolution::SevenTwentyP]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
duration_seconds_options: Some(vec![4, 8, 12]),
|
||||
duration_seconds_default: Some(4),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Sora2Pro,
|
||||
model_creator: Some(ModelCreator::OpenAi),
|
||||
full_name: Some("Sora 2 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
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_options: Some(vec![4, 8, 12]),
|
||||
duration_seconds_default: Some(4),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
+6
-71
@@ -8,71 +8,6 @@ use enums::common::generation::model_creator::ModelCreator;
|
||||
pub fn veo_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
// TODO(bt,2026-04-10): Veo 2 image-to-video doesn't support aspect ratio
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo2,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 2".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
// TODO(bt,2026-04-10): Veo 2 image-to-video doesn't support aspect ratio
|
||||
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 {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo3,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("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::Auto, // TODO: Only for image-to-video
|
||||
]),
|
||||
// TODO: image-to-video aspect ratio options
|
||||
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,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("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,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
@@ -97,8 +32,8 @@ pub fn veo_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
CommonResolution::FourK,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(8),
|
||||
// fal accepts only 4s/6s/8s (reference-to-video: 8s only; see the router's veo_3p1_common).
|
||||
duration_seconds_options: Some(vec![4, 6, 8]),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -127,8 +62,8 @@ pub fn veo_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
CommonResolution::FourK,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(8),
|
||||
// fal accepts only 4s/6s/8s (reference-to-video: 8s only; see the router's veo_3p1_common).
|
||||
duration_seconds_options: Some(vec![4, 6, 8]),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -152,8 +87,8 @@ pub fn veo_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(8),
|
||||
// fal accepts only 4s/6s/8s (reference-to-video: 8s only; see the router's veo_3p1_common).
|
||||
duration_seconds_options: Some(vec![4, 6, 8]),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
+14
-38
@@ -12,7 +12,6 @@ use super::by_type::minimax_video_models::minimax_video_models;
|
||||
use super::by_type::seedance_1x_video_models::{seedance_1p0_video_models, seedance_1p5_video_models};
|
||||
use super::by_type::seedance_2p0_video_models::seedance_2p0_video_models;
|
||||
use super::by_type::seedance_2p5_video_models::seedance_2p5_video_models;
|
||||
use super::by_type::sora_video_models::sora_video_models;
|
||||
use super::by_type::veo_video_models::veo_video_models;
|
||||
use super::by_type::vidu_video_models::vidu_video_models;
|
||||
|
||||
@@ -39,7 +38,6 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
models.extend(seedance_1p5_video_models());
|
||||
models.extend(seedance_2p0_video_models());
|
||||
models.extend(seedance_2p5_video_models());
|
||||
models.extend(sora_video_models());
|
||||
models.extend(veo_video_models());
|
||||
models.extend(vidu_video_models());
|
||||
models.extend(wan_video_models());
|
||||
@@ -63,30 +61,6 @@ fn build_omni_gen_video_model_providers() -> Vec<OmniGenVideoModelProviderDetail
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Seedance10Lite,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Sora2,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Sora2Pro,
|
||||
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,
|
||||
@@ -127,18 +101,6 @@ fn build_omni_gen_video_model_providers() -> Vec<OmniGenVideoModelProviderDetail
|
||||
model: CommonVideoModel::Flux3Draft,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling16Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling21Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling21Master,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling2p5TurboPro,
|
||||
overrides: None,
|
||||
@@ -164,6 +126,20 @@ fn build_omni_gen_video_model_providers() -> Vec<OmniGenVideoModelProviderDetail
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::http_server::endpoints::omni_gen::shared_utils::video::resolve_video_model::{video_model_status, VideoModelStatus};
|
||||
|
||||
/// Retired and replaced models (see `resolve_video_model`) must not be offered.
|
||||
#[test]
|
||||
fn only_available_models_are_listed() {
|
||||
for details in build_omni_gen_video_models() {
|
||||
assert_eq!(video_model_status(details.model), VideoModelStatus::Available, "{:?}", details.model);
|
||||
}
|
||||
for provider in build_omni_gen_video_model_providers() {
|
||||
for details in provider.models {
|
||||
assert_eq!(video_model_status(details.model), VideoModelStatus::Available, "{:?}", details.model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kinovi_batch_options_are_one_through_four() {
|
||||
|
||||
+5
@@ -15,6 +15,7 @@ use crate::http_server::endpoints::generate::common::payments_error_test::paymen
|
||||
use crate::http_server::endpoints::omni_api::generate::video::check_request::check_request;
|
||||
use crate::http_server::endpoints::omni_api::generate::video::ingest_url_inputs::ingest_url_inputs;
|
||||
use crate::http_server::endpoints::omni_api::shared_utils::video::validate_video_request::validate_video_request;
|
||||
use crate::http_server::endpoints::omni_gen::shared_utils::video::resolve_video_model::resolve_video_model;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::shared_video_generation::{
|
||||
run_authenticated_video_generation, VideoGenerationAuth,
|
||||
};
|
||||
@@ -49,6 +50,10 @@ pub async fn omni_api_video_generate_handler(
|
||||
|
||||
info!("request: {:?}", request);
|
||||
|
||||
// Retired models get a 400; replaced ones are rewritten (see helper) before
|
||||
// anything prices, validates, or bills the request.
|
||||
request.model = resolve_video_model(request.model)?;
|
||||
|
||||
// Validate URL/media-token preconditions before any billable or DB-mutating work.
|
||||
check_request(&request)?;
|
||||
|
||||
|
||||
+7
-1
@@ -9,6 +9,7 @@ use artcraft_router::api::router_provider::RouterProvider;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::omni_gen::shared_utils::map_router_cost_error::map_router_cost_error;
|
||||
use crate::http_server::endpoints::omni_gen::generate::mesh::helpers::hydrate_router_request::hydrate_to_router_request;
|
||||
use crate::http_server::endpoints::omni_gen::shared_utils::mesh::resolve_mesh_model::resolve_mesh_model;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Estimate the cost of a mesh (3D object) generation.
|
||||
@@ -25,13 +26,18 @@ use crate::state::server_state::ServerState;
|
||||
)]
|
||||
pub async fn omni_gen_mesh_cost_handler(
|
||||
_http_request: HttpRequest,
|
||||
request: Json<OmniGenMeshCostAndGenerateRequest>,
|
||||
mut request: Json<OmniGenMeshCostAndGenerateRequest>,
|
||||
_server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<OmniGenMeshCostResponse>, CommonWebError> {
|
||||
// NB: Deliberately no input validation here. The UI polls this endpoint
|
||||
// while the user is still composing the request (no prompt typed, nothing
|
||||
// attached), and pricing is a total function of the model and options.
|
||||
// Bad requests are rejected by the generate endpoint.
|
||||
//
|
||||
// The one exception: replaced models are priced as their replacement (the
|
||||
// model generation will actually use).
|
||||
request.model = resolve_mesh_model(request.model)?;
|
||||
|
||||
let mut builder = hydrate_to_router_request(&request)?;
|
||||
|
||||
builder.provider = RouterProvider::Artcraft; // NB: User is paying for ArtCraft credits / generation
|
||||
|
||||
+69
-2
@@ -16,6 +16,7 @@ use crate::http_server::endpoints::generate::common::probed_reference_videos::{
|
||||
};
|
||||
use crate::http_server::endpoints::omni_gen::shared_utils::map_router_cost_error::map_router_cost_error;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::helpers::hydrate_router_request::hydrate_to_router_request;
|
||||
use crate::http_server::endpoints::omni_gen::shared_utils::video::resolve_video_model::resolve_video_model;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Estimate the cost of a video generation.
|
||||
@@ -32,13 +33,18 @@ use crate::state::server_state::ServerState;
|
||||
)]
|
||||
pub async fn omni_gen_video_cost_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<OmniGenVideoCostAndGenerateRequest>,
|
||||
mut request: Json<OmniGenVideoCostAndGenerateRequest>,
|
||||
maybe_server_state: Option<web::Data<Arc<ServerState>>>,
|
||||
) -> Result<Json<OmniGenVideoCostResponse>, CommonWebError> {
|
||||
// NB: Deliberately no input validation here. The UI polls this endpoint
|
||||
// while the user is still composing the request (no prompt typed, nothing
|
||||
// attached), and pricing is a total function of the model and options.
|
||||
// Bad requests are rejected by the generate endpoint.
|
||||
//
|
||||
// The one exception: retired models get a 400, and replaced ones are priced
|
||||
// as their replacement (the model generation will actually use).
|
||||
request.model = resolve_video_model(request.model)?;
|
||||
|
||||
let mut builder = hydrate_to_router_request(&request)?;
|
||||
|
||||
builder.provider = RouterProvider::Artcraft; // NB: User is paying for ArtCraft credits / generation
|
||||
@@ -216,7 +222,6 @@ mod tests {
|
||||
CommonVideoModel::Kling3p0Pro,
|
||||
CommonVideoModel::MinimaxH3,
|
||||
CommonVideoModel::Seedance2p0,
|
||||
CommonVideoModel::Sora2,
|
||||
CommonVideoModel::Veo3p1,
|
||||
CommonVideoModel::ViduQ3,
|
||||
] {
|
||||
@@ -228,6 +233,68 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
mod retired_and_replaced_model_tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn sora_models_are_rejected_with_a_400() {
|
||||
for model in [CommonVideoModel::Sora2, CommonVideoModel::Sora2Pro] {
|
||||
let err = post_cost_request(base_request(model))
|
||||
.await
|
||||
.expect_err("retired model should be rejected");
|
||||
assert_eq!(err.status_code(), StatusCode::BAD_REQUEST, "{:?}", model);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replaced_models_are_quoted_as_their_replacement() {
|
||||
for (model, replacement) in [
|
||||
(CommonVideoModel::Seedance2p5Preview, CommonVideoModel::Seedance2p5),
|
||||
(CommonVideoModel::Veo3Fast, CommonVideoModel::Veo3p1Fast),
|
||||
(CommonVideoModel::Veo3, CommonVideoModel::Veo3p1),
|
||||
(CommonVideoModel::Kling21Master, CommonVideoModel::Kling2p6Pro),
|
||||
] {
|
||||
let replaced_quote = post_cost_request(base_request(model)).await.unwrap();
|
||||
let replacement_quote = post_cost_request(base_request(replacement)).await.unwrap();
|
||||
assert_eq!(replaced_quote.cost_in_credits, replacement_quote.cost_in_credits, "{:?}", model);
|
||||
assert!(replaced_quote.cost_in_credits.unwrap() > 0, "{:?}", model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod veo_duration_tests {
|
||||
use super::*;
|
||||
|
||||
/// fal's Veo 3.1 reference-to-video endpoint only generates 8s, so a
|
||||
/// shorter request must be quoted (and billed) as the 8s it produces.
|
||||
#[tokio::test]
|
||||
async fn veo_3p1_reference_requests_are_quoted_at_8s() {
|
||||
for model in [CommonVideoModel::Veo3p1, CommonVideoModel::Veo3p1Fast] {
|
||||
let quote = |seconds| {
|
||||
let mut request = base_request(model);
|
||||
request.reference_image_media_tokens = Some(vec![MediaFileToken::new_from_str("m_reference")]);
|
||||
request.duration_seconds = Some(seconds);
|
||||
post_cost_request(request)
|
||||
};
|
||||
let four = quote(4).await.unwrap();
|
||||
let eight = quote(8).await.unwrap();
|
||||
assert_eq!(four.cost_in_credits, eight.cost_in_credits, "{:?}", model);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn veo_3p1_text_to_video_quotes_by_duration() {
|
||||
let quote = |seconds| {
|
||||
let mut request = base_request(CommonVideoModel::Veo3p1Fast);
|
||||
request.duration_seconds = Some(seconds);
|
||||
post_cost_request(request)
|
||||
};
|
||||
let four = quote(4).await.unwrap().cost_in_credits.unwrap();
|
||||
let eight = quote(8).await.unwrap().cost_in_credits.unwrap();
|
||||
assert!(four < eight);
|
||||
}
|
||||
}
|
||||
|
||||
mod error_mapping_tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
+6
-1
@@ -35,6 +35,7 @@ use crate::http_server::endpoints::omni_gen::generate::mesh::helpers::hydrate_ro
|
||||
use crate::http_server::endpoints::omni_gen::generate::mesh::insert_db_job::insert_fal_job::{insert_fal_job, InsertFalJobArgs};
|
||||
use crate::http_server::endpoints::omni_gen::generate::mesh::pipeline_v2::run_pipeline_v2::{run_pipeline_v2, RunPipelineV2Args};
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::shared_job_args::SharedJobArgs;
|
||||
use crate::http_server::endpoints::omni_gen::shared_utils::mesh::resolve_mesh_model::resolve_mesh_model;
|
||||
use crate::http_server::endpoints::omni_gen::shared_utils::mesh::validate_mesh_request::validate_mesh_request;
|
||||
use crate::http_server::user_lookup::api_or_web_session::require_any_session_or_key::{require_any_session_or_key, AnySessionType};
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
@@ -60,12 +61,16 @@ use crate::util::lookup::lookup_media_files_as_cdn_url_list_and_map::lookup_medi
|
||||
)]
|
||||
pub async fn omni_gen_mesh_generate_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<OmniGenMeshCostAndGenerateRequest>,
|
||||
mut request: Json<OmniGenMeshCostAndGenerateRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<OmniGenMeshGenerateResponse>, CommonWebError> {
|
||||
|
||||
info!("request: {:?}", request);
|
||||
|
||||
// Replaced models are rewritten (see helper) before anything prices,
|
||||
// validates, or bills the request.
|
||||
request.model = resolve_mesh_model(request.model)?;
|
||||
|
||||
// Reject doomed combos (e.g. prompt-only hunyuan 2.x, sketch without a
|
||||
// prompt) before any billable or DB-mutating work — see helper for the rules.
|
||||
validate_mesh_request(&request)?;
|
||||
|
||||
+6
-1
@@ -13,6 +13,7 @@ use crate::http_server::endpoints::generate::common::payments_error_test::paymen
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::shared_video_generation::{
|
||||
run_authenticated_video_generation, VideoGenerationAuth,
|
||||
};
|
||||
use crate::http_server::endpoints::omni_gen::shared_utils::video::resolve_video_model::resolve_video_model;
|
||||
use crate::http_server::endpoints::omni_gen::shared_utils::video::validate_video_request::validate_video_request;
|
||||
use crate::http_server::user_lookup::api_or_web_session::require_any_session_or_key::{require_any_session_or_key, AnySessionType};
|
||||
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
|
||||
@@ -40,12 +41,16 @@ use crate::state::server_state::ServerState;
|
||||
)]
|
||||
pub async fn omni_gen_video_generate_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<OmniGenVideoCostAndGenerateRequest>,
|
||||
mut request: Json<OmniGenVideoCostAndGenerateRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<OmniGenVideoGenerateResponse>, CommonWebError> {
|
||||
|
||||
info!("request: {:?}", request);
|
||||
|
||||
// Retired models get a 400; replaced ones are rewritten (see helper) before
|
||||
// anything prices, validates, or bills the request.
|
||||
request.model = resolve_video_model(request.model)?;
|
||||
|
||||
// Reject doomed combos (e.g. grok_imagine_video_1p5 without an image)
|
||||
// before any billable or DB-mutating work — see helper for the rules.
|
||||
validate_video_request(&request)?;
|
||||
|
||||
+1
@@ -1 +1,2 @@
|
||||
pub mod resolve_mesh_model;
|
||||
pub mod validate_mesh_request;
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
|
||||
/// Whether a mesh model can still be generated, and how.
|
||||
///
|
||||
/// The `CommonMeshModel` variants below stay in the enum forever: they're
|
||||
/// stored in historical database records. They're only removed from the model
|
||||
/// list (`configs::omni_gen::mesh_models`) and handled here on requests.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum MeshModelStatus {
|
||||
Available,
|
||||
/// Fulfilled (and priced) as a different model.
|
||||
ReplacedBy(CommonMeshModel),
|
||||
}
|
||||
|
||||
/// Resolve the requested model before any pricing, validation, or billing:
|
||||
/// replaced models are rewritten to their replacement. Call this first in
|
||||
/// every mesh generate/cost handler.
|
||||
///
|
||||
/// (No mesh model is retired outright yet. When one is, add a `Retired`
|
||||
/// status that returns a 400, like `resolve_video_model`.)
|
||||
pub fn resolve_mesh_model(
|
||||
maybe_model: Option<CommonMeshModel>,
|
||||
) -> Result<Option<CommonMeshModel>, CommonWebError> {
|
||||
Ok(maybe_model.map(|model| match mesh_model_status(model) {
|
||||
MeshModelStatus::Available => model,
|
||||
MeshModelStatus::ReplacedBy(replacement) => replacement,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Replaced models were pulled by their upstream provider; fal marks their
|
||||
/// endpoints `deprecated` (checked 2026-09-28 against fal's model API).
|
||||
pub fn mesh_model_status(model: CommonMeshModel) -> MeshModelStatus {
|
||||
match model {
|
||||
// Hunyuan 3D 2.1 endpoint deprecated (2026-09-28). Hunyuan 3D 3 also
|
||||
// takes a single input image.
|
||||
CommonMeshModel::Hunyuan3d2p1 => MeshModelStatus::ReplacedBy(CommonMeshModel::Hunyuan3d3),
|
||||
_ => MeshModelStatus::Available,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hunyuan_3d_2p1_is_replaced_by_hunyuan_3d_3() {
|
||||
assert_eq!(resolve_mesh_model(Some(CommonMeshModel::Hunyuan3d2p1)).unwrap(), Some(CommonMeshModel::Hunyuan3d3));
|
||||
assert_eq!(mesh_model_status(CommonMeshModel::Hunyuan3d3), MeshModelStatus::Available);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn available_and_missing_models_pass_through() {
|
||||
assert_eq!(resolve_mesh_model(Some(CommonMeshModel::Hunyuan3d2p0)).unwrap(), Some(CommonMeshModel::Hunyuan3d2p0));
|
||||
assert_eq!(resolve_mesh_model(None).unwrap(), None);
|
||||
}
|
||||
}
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
pub(crate) mod validate_kinovi_batch_count;
|
||||
pub mod resolve_video_model;
|
||||
mod validate_seedance_2p0_limits;
|
||||
pub mod validate_video_request;
|
||||
mod validate_when_image_required;
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
|
||||
/// Whether a video model can still be generated, and how.
|
||||
///
|
||||
/// The `CommonVideoModel` variants below stay in the enum forever: they're
|
||||
/// stored in historical database records. They're only removed from the model
|
||||
/// list (`configs::omni_gen::video_models`) and handled here on requests.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum VideoModelStatus {
|
||||
Available,
|
||||
/// Fulfilled (and priced) as a different model.
|
||||
ReplacedBy(CommonVideoModel),
|
||||
/// No longer offered; requests get a 400.
|
||||
Retired { display_name: &'static str },
|
||||
}
|
||||
|
||||
/// Resolve the requested model before any pricing, validation, or billing:
|
||||
/// replaced models are rewritten to their replacement, and retired models are
|
||||
/// rejected with a 400. Call this first in every video generate/cost handler.
|
||||
pub fn resolve_video_model(
|
||||
maybe_model: Option<CommonVideoModel>,
|
||||
) -> Result<Option<CommonVideoModel>, CommonWebError> {
|
||||
let Some(model) = maybe_model else {
|
||||
return Ok(None);
|
||||
};
|
||||
match video_model_status(model) {
|
||||
VideoModelStatus::Available => Ok(Some(model)),
|
||||
VideoModelStatus::ReplacedBy(replacement) => Ok(Some(replacement)),
|
||||
VideoModelStatus::Retired { display_name } => Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
format!("{display_name} is no longer available. Please choose a different video model."))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retired models were pulled by their upstream provider; fal marks their
|
||||
/// endpoints `deprecated` (checked 2026-09-28 against fal's model API).
|
||||
/// Where a newer model in the same family is available, requests are upgraded
|
||||
/// to it instead of failing.
|
||||
pub fn video_model_status(model: CommonVideoModel) -> VideoModelStatus {
|
||||
match model {
|
||||
// Sora 2 is no longer available from the API (2026-09-28).
|
||||
CommonVideoModel::Sora2 => VideoModelStatus::Retired { display_name: "Sora 2" },
|
||||
CommonVideoModel::Sora2Pro => VideoModelStatus::Retired { display_name: "Sora 2 Pro" },
|
||||
|
||||
// Veo 2, Veo 3 and Veo 3 Fast endpoints deprecated (2026-09-28).
|
||||
CommonVideoModel::Veo2 => VideoModelStatus::ReplacedBy(CommonVideoModel::Veo3p1),
|
||||
CommonVideoModel::Veo3 => VideoModelStatus::ReplacedBy(CommonVideoModel::Veo3p1),
|
||||
CommonVideoModel::Veo3Fast => VideoModelStatus::ReplacedBy(CommonVideoModel::Veo3p1Fast),
|
||||
|
||||
// Kling 1.6 Pro and 2.1 Pro/Master endpoints deprecated (2026-09-28).
|
||||
CommonVideoModel::Kling16Pro => VideoModelStatus::ReplacedBy(CommonVideoModel::Kling2p5TurboPro),
|
||||
CommonVideoModel::Kling21Pro => VideoModelStatus::ReplacedBy(CommonVideoModel::Kling2p5TurboPro),
|
||||
CommonVideoModel::Kling21Master => VideoModelStatus::ReplacedBy(CommonVideoModel::Kling2p6Pro),
|
||||
|
||||
// Seedance 1.0 Lite endpoint deprecated (2026-09-28).
|
||||
CommonVideoModel::Seedance10Lite => VideoModelStatus::ReplacedBy(CommonVideoModel::Seedance1p5Pro),
|
||||
|
||||
// Seedance 2.5 Preview was superseded by Seedance 2.5 (2026-09-28). It's
|
||||
// fulfilled and priced as Seedance 2.5, which is cheaper.
|
||||
CommonVideoModel::Seedance2p5Preview => VideoModelStatus::ReplacedBy(CommonVideoModel::Seedance2p5),
|
||||
|
||||
_ => VideoModelStatus::Available,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod retired {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sora_models_are_rejected_with_a_400() {
|
||||
for (model, name) in [(CommonVideoModel::Sora2, "Sora 2"), (CommonVideoModel::Sora2Pro, "Sora 2 Pro")] {
|
||||
match resolve_video_model(Some(model)) {
|
||||
Err(CommonWebError::BadInputWithSimpleMessage(message)) => {
|
||||
assert_eq!(message, format!("{name} is no longer available. Please choose a different video model."));
|
||||
}
|
||||
other => panic!("expected a 400 for {:?}, got {:?}", model, other),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod replaced {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn replaced_models_resolve_to_their_replacement() {
|
||||
for (model, replacement) in [
|
||||
(CommonVideoModel::Veo2, CommonVideoModel::Veo3p1),
|
||||
(CommonVideoModel::Veo3, CommonVideoModel::Veo3p1),
|
||||
(CommonVideoModel::Veo3Fast, CommonVideoModel::Veo3p1Fast),
|
||||
(CommonVideoModel::Kling16Pro, CommonVideoModel::Kling2p5TurboPro),
|
||||
(CommonVideoModel::Kling21Pro, CommonVideoModel::Kling2p5TurboPro),
|
||||
(CommonVideoModel::Kling21Master, CommonVideoModel::Kling2p6Pro),
|
||||
(CommonVideoModel::Seedance10Lite, CommonVideoModel::Seedance1p5Pro),
|
||||
(CommonVideoModel::Seedance2p5Preview, CommonVideoModel::Seedance2p5),
|
||||
] {
|
||||
assert_eq!(resolve_video_model(Some(model)).unwrap(), Some(replacement), "{model:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replacements_are_themselves_available() {
|
||||
for model in [
|
||||
CommonVideoModel::Veo2, CommonVideoModel::Veo3, CommonVideoModel::Veo3Fast,
|
||||
CommonVideoModel::Kling16Pro, CommonVideoModel::Kling21Pro, CommonVideoModel::Kling21Master,
|
||||
CommonVideoModel::Seedance10Lite, CommonVideoModel::Seedance2p5Preview,
|
||||
] {
|
||||
let VideoModelStatus::ReplacedBy(replacement) = video_model_status(model) else {
|
||||
panic!("{:?} should be replaced", model);
|
||||
};
|
||||
assert_eq!(video_model_status(replacement), VideoModelStatus::Available, "{model:?} -> {replacement:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn available_and_missing_models_pass_through() {
|
||||
assert_eq!(resolve_video_model(Some(CommonVideoModel::Veo3p1Fast)).unwrap(), Some(CommonVideoModel::Veo3p1Fast));
|
||||
assert_eq!(resolve_video_model(Some(CommonVideoModel::Seedance2p5)).unwrap(), Some(CommonVideoModel::Seedance2p5));
|
||||
assert_eq!(resolve_video_model(None).unwrap(), None);
|
||||
}
|
||||
}
|
||||
@@ -1158,11 +1158,6 @@ export const PromptBoxVideo = ({
|
||||
case "grok_imagine_video":
|
||||
request.grok_aspect_ratio = getGrokAspectRatio();
|
||||
break;
|
||||
|
||||
case "sora_2":
|
||||
request.sora_orientation =
|
||||
resolution === "720p" ? "landscape" : "portrait";
|
||||
break;
|
||||
}
|
||||
|
||||
if (selectedModel.supportsCommonAspectRatio) {
|
||||
|
||||
@@ -195,65 +195,6 @@ const ALL_MODELS: ModelConfig[] = [
|
||||
tags: [ModelTag.MaskedInpainting],
|
||||
}),
|
||||
|
||||
//////////////////////////////
|
||||
// Video models
|
||||
//////////////////////////////
|
||||
cfg({
|
||||
id: "kling_1_6_pro",
|
||||
category: "video",
|
||||
info: {
|
||||
name: "Kling 1.6 Pro",
|
||||
tauri_id: "kling_1.6_pro",
|
||||
creator: mc.Kling,
|
||||
},
|
||||
description: "Good quality model",
|
||||
badges: [{ label: "2 min." }],
|
||||
capabilities: { maxGenerationCount: 1 },
|
||||
}),
|
||||
cfg({
|
||||
id: "kling_2_1_pro",
|
||||
category: "video",
|
||||
info: {
|
||||
name: "Kling 2.1 Pro",
|
||||
tauri_id: "kling_2.1_pro",
|
||||
creator: mc.Kling,
|
||||
},
|
||||
description: "High quality model",
|
||||
badges: [{ label: "2 min." }],
|
||||
capabilities: { maxGenerationCount: 1 },
|
||||
}),
|
||||
cfg({
|
||||
id: "kling_2_1_master",
|
||||
category: "video",
|
||||
info: {
|
||||
name: "Kling 2.1 Master",
|
||||
tauri_id: "kling_2.1_master",
|
||||
creator: mc.Kling,
|
||||
},
|
||||
description: "Master quality model ($$)",
|
||||
badges: [{ label: "2 min." }],
|
||||
capabilities: { maxGenerationCount: 1 },
|
||||
}),
|
||||
cfg({
|
||||
id: "seedance_1_0_lite",
|
||||
category: "video",
|
||||
info: {
|
||||
name: "Seedance 1.0 Lite",
|
||||
tauri_id: "seedance_1.0_lite",
|
||||
creator: mc.Bytedance,
|
||||
},
|
||||
description: "Fast and high-quality model",
|
||||
badges: [{ label: "2 min." }],
|
||||
capabilities: { maxGenerationCount: 1 },
|
||||
}),
|
||||
cfg({
|
||||
id: "veo_2",
|
||||
category: "video",
|
||||
info: { name: "Google Veo 2", tauri_id: "veo_2", creator: mc.Google },
|
||||
description: "Fast and high-quality model",
|
||||
badges: [{ label: "2 min." }],
|
||||
capabilities: { maxGenerationCount: 1 },
|
||||
}),
|
||||
];
|
||||
|
||||
// export const ALL_MODELS_BY_ID: Map<string, ModelConfig> = new Map(
|
||||
|
||||
@@ -75,54 +75,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
},
|
||||
],
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "kling_1_6_pro",
|
||||
tauriId: "kling_1p6_pro",
|
||||
fullName: "Kling 1.6 Pro",
|
||||
category: "video",
|
||||
creator: ModelCreator.Kling,
|
||||
selectorName: "Kling 1.6 Pro",
|
||||
selectorDescription: "Fast video model",
|
||||
selectorBadges: ["2 min."],
|
||||
startFrame: true,
|
||||
endFrame: true,
|
||||
requiresImage: true,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: false,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "kling_2_1_pro",
|
||||
tauriId: "kling_2p1_pro",
|
||||
fullName: "Kling 2.1 Pro",
|
||||
category: "video",
|
||||
creator: ModelCreator.Kling,
|
||||
selectorName: "Kling 2.1 Pro",
|
||||
selectorDescription: "Fast video model",
|
||||
selectorBadges: ["2 min."],
|
||||
startFrame: true,
|
||||
endFrame: true,
|
||||
requiresImage: true,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: false,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "kling_2_1_master",
|
||||
tauriId: "kling_2p1_master",
|
||||
fullName: "Kling 2.1 Master",
|
||||
category: "video",
|
||||
creator: ModelCreator.Kling,
|
||||
selectorName: "Kling 2.1 Master",
|
||||
selectorDescription: "Fast video model",
|
||||
selectorBadges: ["2 min."],
|
||||
startFrame: true,
|
||||
endFrame: false,
|
||||
requiresImage: true,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: false,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "kling_2p5_turbo_pro",
|
||||
tauriId: "kling_2p5_turbo_pro",
|
||||
@@ -274,21 +226,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: true,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "seedance_1_0_lite",
|
||||
tauriId: "seedance_1p0_lite",
|
||||
fullName: "Seedance 1.0 Lite",
|
||||
category: "video",
|
||||
creator: ModelCreator.Bytedance,
|
||||
selectorName: "Seedance 1.0 Lite",
|
||||
selectorDescription: "Fast video model",
|
||||
selectorBadges: ["2 min."],
|
||||
startFrame: true,
|
||||
endFrame: false,
|
||||
requiresImage: true,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 3000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "seedance_1p5_pro",
|
||||
tauriId: "seedance_1p5_pro",
|
||||
@@ -460,109 +397,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
progressBarTime: 300000, // ~5 minutes
|
||||
maxPromptLength: Infinity,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "sora_2",
|
||||
tauriId: "sora_2",
|
||||
fullName: "Sora 2",
|
||||
category: "video",
|
||||
creator: ModelCreator.OpenAi,
|
||||
providers: [GenerationProvider.Artcraft],
|
||||
selectorName: "Sora 2",
|
||||
selectorDescription: "Smart video model",
|
||||
selectorBadges: ["2 min."],
|
||||
startFrame: false,
|
||||
endFrame: false,
|
||||
requiresImage: false,
|
||||
sizeOptions: [
|
||||
{
|
||||
tauriValue: "landscape",
|
||||
textLabel: "Landscape",
|
||||
icon: SizeIconOption.Landscape,
|
||||
},
|
||||
{
|
||||
tauriValue: "portrait",
|
||||
textLabel: "Portrait",
|
||||
icon: SizeIconOption.Portrait,
|
||||
},
|
||||
],
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 2000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "sora_2_pro",
|
||||
tauriId: "sora_2_pro",
|
||||
fullName: "Sora 2 Pro",
|
||||
category: "video",
|
||||
creator: ModelCreator.OpenAi,
|
||||
selectorName: "Sora 2 Pro",
|
||||
selectorDescription: "Smart video model",
|
||||
selectorBadges: ["2 min."],
|
||||
startFrame: false,
|
||||
endFrame: false,
|
||||
requiresImage: false,
|
||||
sizeOptions: [
|
||||
{
|
||||
tauriValue: "landscape",
|
||||
textLabel: "Landscape",
|
||||
icon: SizeIconOption.Landscape,
|
||||
},
|
||||
{
|
||||
tauriValue: "portrait",
|
||||
textLabel: "Portrait",
|
||||
icon: SizeIconOption.Portrait,
|
||||
},
|
||||
],
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 2000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_2",
|
||||
tauriId: "veo_2",
|
||||
fullName: "Google Veo 2",
|
||||
category: "video",
|
||||
creator: ModelCreator.Google,
|
||||
selectorName: "Google Veo 2",
|
||||
selectorDescription: "Fast video model",
|
||||
selectorBadges: ["2 min."],
|
||||
startFrame: true,
|
||||
endFrame: false,
|
||||
requiresImage: true,
|
||||
generateWithSound: false,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 5000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_3",
|
||||
tauriId: "veo_3",
|
||||
fullName: "Google Veo 3",
|
||||
category: "video",
|
||||
creator: ModelCreator.Google,
|
||||
selectorName: "Google Veo 3",
|
||||
selectorDescription: "Slow, high-quality model",
|
||||
selectorBadges: ["2 min."],
|
||||
startFrame: true,
|
||||
endFrame: false,
|
||||
requiresImage: true,
|
||||
generateWithSound: true,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 5000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_3_fast",
|
||||
tauriId: "veo_3_fast",
|
||||
fullName: "Google Veo 3 Fast",
|
||||
category: "video",
|
||||
creator: ModelCreator.Google,
|
||||
selectorName: "Google Veo 3 Fast",
|
||||
selectorDescription: "High-quality model (faster)",
|
||||
selectorBadges: ["2 min."],
|
||||
startFrame: true,
|
||||
endFrame: false,
|
||||
requiresImage: true,
|
||||
generateWithSound: true,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 5000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_3p1",
|
||||
tauriId: "veo_3p1",
|
||||
|
||||
@@ -19,7 +19,6 @@ describe("desktop catalog", () => {
|
||||
const ids = [
|
||||
"seedance_2p5",
|
||||
"seedance_2p5_u",
|
||||
"seedance_2p5_preview",
|
||||
"seedance_2p0_mini",
|
||||
"flux_3",
|
||||
"minimax_h3",
|
||||
@@ -34,13 +33,13 @@ describe("desktop catalog", () => {
|
||||
});
|
||||
|
||||
it("retains offered disabled entries and excludes the MiniMax admin variants", () => {
|
||||
const ids = ["sora_2", "minimax_h3_turbo", "minimax_h3_ultra"];
|
||||
const ids = ["kling_3p0_pro", "minimax_h3_turbo", "minimax_h3_ultra"];
|
||||
const models = buildVideoModelsFromListing(
|
||||
[],
|
||||
ids.map((model) => ({ model, is_disabled: true })),
|
||||
ids,
|
||||
);
|
||||
expect(models.map((model) => model.tauriId)).toEqual(["sora_2"]);
|
||||
expect(models.map((model) => model.tauriId)).toEqual(["kling_3p0_pro"]);
|
||||
});
|
||||
|
||||
it("preserves future image options and leaves editor eligibility unchanged", () => {
|
||||
@@ -72,16 +71,16 @@ describe("desktop catalog", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps Sora and Marble models available through ArtCraft after hydration", () => {
|
||||
it("keeps GPT Image, Veo and Marble models available through ArtCraft after hydration", () => {
|
||||
const images = buildImageModelsFromListing(IMAGE_MODELS, [
|
||||
{ model: "gpt_image_1", image_refs_supported: true },
|
||||
]);
|
||||
const videos = buildVideoModelsFromListing(VIDEO_MODELS, [
|
||||
{ model: "sora_2", duration_seconds_options: [4, 8, 12] },
|
||||
{ model: "veo_3p1", duration_seconds_options: [4, 6, 8] },
|
||||
]);
|
||||
expect(images.find((model) => model.tauriId === "gpt_image_1")?.getProviders())
|
||||
.toEqual([GenerationProvider.Artcraft]);
|
||||
expect(videos.find((model) => model.tauriId === "sora_2")?.getProviders())
|
||||
expect(videos.find((model) => model.tauriId === "veo_3p1")?.getProviders())
|
||||
.toEqual([GenerationProvider.Artcraft]);
|
||||
expect(SPLAT_MODELS.map((model) => model.tauriId)).toEqual([
|
||||
"marble_0p1_mini", "marble_0p1_plus",
|
||||
|
||||
Reference in new issue
Block a user