mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
artcraft router
This commit is contained in:
@@ -35,6 +35,9 @@ pub enum RouterVideoModel {
|
||||
#[serde(rename = "kling_3p0_pro")]
|
||||
Kling3p0Pro,
|
||||
|
||||
#[serde(rename = "minimax_h3")]
|
||||
MinimaxH3,
|
||||
|
||||
#[serde(rename = "seedance_1p0_lite")]
|
||||
Seedance10Lite,
|
||||
|
||||
@@ -149,4 +152,11 @@ mod tests {
|
||||
assert_serde_round_trip(RouterVideoModel::ViduQ3, "vidu_q3");
|
||||
assert_serde_round_trip(RouterVideoModel::ViduQ3Turbo, "vidu_q3_turbo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimax_h3_serializes() {
|
||||
// NB: This string must match `CommonVideoModel` — the two enums convert
|
||||
// via serde string round-trip in the omni handlers.
|
||||
assert_serde_round_trip(RouterVideoModel::MinimaxH3, "minimax_h3");
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -43,6 +43,7 @@ use crate::generate::generate_video::providers::artcraft::veo_3_fast::build::bui
|
||||
use crate::generate::generate_video::providers::artcraft::veo_3p1::build::build_artcraft_veo_3p1;
|
||||
use crate::generate::generate_video::providers::artcraft::veo_3p1_fast::build::build_artcraft_veo_3p1_fast;
|
||||
use crate::generate::generate_video::providers::artcraft::veo_3p1_lite::build::build_artcraft_veo_3p1_lite;
|
||||
use crate::generate::generate_video::providers::artcraft::minimax_h3::build::build_artcraft_minimax_h3;
|
||||
use crate::generate::generate_video::providers::artcraft::vidu_q3::build::build_artcraft_vidu_q3;
|
||||
use crate::generate::generate_video::providers::artcraft::vidu_q3_turbo::build::build_artcraft_vidu_q3_turbo;
|
||||
use crate::generate::generate_video::providers::kinovi::happy_horse_1p0::build::build_kinovi_happy_horse_1p0;
|
||||
@@ -68,6 +69,7 @@ use crate::generate::generate_video::providers::fal::veo_3_fast::build::build_fa
|
||||
use crate::generate::generate_video::providers::fal::veo_3p1::build::build_fal_veo_3p1;
|
||||
use crate::generate::generate_video::providers::fal::veo_3p1_fast::build::build_fal_veo_3p1_fast;
|
||||
use crate::generate::generate_video::providers::fal::veo_3p1_lite::build::build_fal_veo_3p1_lite;
|
||||
use crate::generate::generate_video::providers::fal::minimax_h3::build::build_fal_minimax_h3;
|
||||
use crate::generate::generate_video::providers::fal::vidu_q3::build::build_fal_vidu_q3;
|
||||
use crate::generate::generate_video::providers::fal::vidu_q3_turbo::build::build_fal_vidu_q3_turbo;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast;
|
||||
@@ -180,6 +182,7 @@ impl GenerateVideoRequestBuilder {
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Kling2p6Pro) => build_artcraft_kling_2_6_pro(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Kling3p0Pro) => build_artcraft_kling_3p0_pro(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Kling3p0Standard) => build_artcraft_kling_3p0_standard(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::MinimaxH3) => build_artcraft_minimax_h3(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0) => build_artcraft_seedance_2p0(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0Fast) => build_artcraft_seedance_2p0_fast(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0Ultra) => build_artcraft_seedance_2p0_u(self),
|
||||
@@ -214,6 +217,7 @@ impl GenerateVideoRequestBuilder {
|
||||
(RouterProvider::Fal, RouterVideoModel::Kling2p6Pro) => build_fal_kling_2_6_pro(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Kling3p0Pro) => build_fal_kling_3p0_pro(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Kling3p0Standard) => build_fal_kling_3p0_standard(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::MinimaxH3) => build_fal_minimax_h3(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Seedance10Lite) => build_fal_seedance_1p0_lite(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Seedance1p5Pro) => build_fal_seedance_1p5_pro(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Sora2) => build_fal_sora_2(self),
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
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::artcraft::minimax_h3::request::ArtcraftMinimaxH3RequestState;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_minimax_h3(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let state = build_artcraft_minimax_h3_state(builder)?;
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftMinimaxH3(state)))
|
||||
}
|
||||
|
||||
pub(crate) fn build_artcraft_minimax_h3_state(builder: GenerateVideoRequestBuilder) -> Result<ArtcraftMinimaxH3RequestState, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::MinimaxH3,
|
||||
SupportedResolutions::Full,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
Ok(ArtcraftMinimaxH3RequestState { request })
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
|
||||
use crate::generate::generate_video::providers::artcraft::minimax_h3::request::ArtcraftMinimaxH3RequestState;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
/// Per-second rates in hundredths of a US cent.
|
||||
const LOW_RES_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 1_840;
|
||||
const HIGH_RES_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 2_990;
|
||||
|
||||
/// The first 5 reference images are included; each additional image bills a
|
||||
/// flat fee (in hundredths of a US cent).
|
||||
const INCLUDED_REFERENCE_IMAGES: u64 = 5;
|
||||
const EXTRA_REFERENCE_IMAGE_HUNDREDTH_CENTS: u64 = 920;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftMinimaxH3CostState {
|
||||
pub duration_seconds: u64,
|
||||
pub is_2k: bool,
|
||||
pub reference_image_count: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftMinimaxH3CostState {
|
||||
pub fn from_request(request: &ArtcraftMinimaxH3RequestState) -> Self {
|
||||
Self {
|
||||
// MiniMax H3 defaults None → 5s.
|
||||
duration_seconds: request.request.duration_seconds.map(u64::from).unwrap_or(5),
|
||||
is_2k: is_2k(request.request.resolution),
|
||||
reference_image_count: request.request.reference_image_media_tokens
|
||||
.as_ref()
|
||||
.map_or(0, |tokens| tokens.len() as u64),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
let rate = if self.is_2k {
|
||||
HIGH_RES_RATE_HUNDREDTH_CENTS_PER_SEC
|
||||
} else {
|
||||
LOW_RES_RATE_HUNDREDTH_CENTS_PER_SEC
|
||||
};
|
||||
let extra_images = self.reference_image_count.saturating_sub(INCLUDED_REFERENCE_IMAGES);
|
||||
// Round up to the next whole cent.
|
||||
let cost_in_usd_cents =
|
||||
(rate * self.duration_seconds + extra_images * EXTRA_REFERENCE_IMAGE_HUNDREDTH_CENTS)
|
||||
.div_ceil(100);
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MiniMax H3 offers 768P and 2K output; 1080p and above land on the 2K tier,
|
||||
/// while 720p and below render at 768P. The default (unset) resolution is 2K.
|
||||
fn is_2k(resolution: Option<CommonResolutionEnum>) -> bool {
|
||||
match resolution {
|
||||
None => true, // defaults to 2K
|
||||
Some(CommonResolutionEnum::TenEightyP)
|
||||
| Some(CommonResolutionEnum::OneK)
|
||||
| Some(CommonResolutionEnum::TwoK)
|
||||
| Some(CommonResolutionEnum::ThreeK)
|
||||
| Some(CommonResolutionEnum::FourK) => true,
|
||||
Some(CommonResolutionEnum::HalfK)
|
||||
| Some(CommonResolutionEnum::FourEightyP)
|
||||
| Some(CommonResolutionEnum::SevenTwentyP) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::artcraft::minimax_h3::build::build_artcraft_minimax_h3_state;
|
||||
use crate::generate::generate_video::providers::artcraft::minimax_h3::cost::ArtcraftMinimaxH3CostState;
|
||||
|
||||
#[test]
|
||||
fn default_resolution_5s_is_150() { assert_eq!(cost_cents(Some(5), None, 0), 150); }
|
||||
|
||||
#[test]
|
||||
fn default_duration_is_5s() { assert_eq!(cost_cents(None, None, 0), 150); }
|
||||
|
||||
#[test]
|
||||
fn low_res_5s_is_92() { assert_eq!(cost_cents(Some(5), Some(RouterResolution::SevenTwentyP), 0), 92); }
|
||||
|
||||
#[test]
|
||||
fn four_eighty_p_lands_on_low_tier() {
|
||||
assert_eq!(cost_cents(Some(5), Some(RouterResolution::FourEightyP), 0), 92);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_res_15s_is_276() { assert_eq!(cost_cents(Some(15), Some(RouterResolution::SevenTwentyP), 0), 276); }
|
||||
|
||||
#[test]
|
||||
fn high_res_10s_is_299() { assert_eq!(cost_cents(Some(10), Some(RouterResolution::TenEightyP), 0), 299); }
|
||||
|
||||
#[test]
|
||||
fn odd_duration_rounds_up_to_whole_cents() {
|
||||
// 7s high res: 2990 × 7 = 20930 hundredth-cents → 210 cents.
|
||||
assert_eq!(cost_cents(Some(7), None, 0), 210);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn five_reference_images_are_included() {
|
||||
assert_eq!(cost_cents(Some(5), None, 5), cost_cents(Some(5), None, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nine_reference_images_add_the_extra_image_fee() {
|
||||
// 2990 × 5 + 4 × 920 = 18630 hundredth-cents → 187 cents.
|
||||
assert_eq!(cost_cents(Some(5), None, 9), 187);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_classifier_defaults_2k() {
|
||||
assert!(super::is_2k(None));
|
||||
assert!(super::is_2k(Some(CommonResolutionEnum::TenEightyP)));
|
||||
assert!(!super::is_2k(Some(CommonResolutionEnum::SevenTwentyP)));
|
||||
assert!(!super::is_2k(Some(CommonResolutionEnum::FourEightyP)));
|
||||
}
|
||||
|
||||
fn cost_cents(
|
||||
duration_seconds: Option<u16>,
|
||||
resolution: Option<RouterResolution>,
|
||||
reference_image_count: usize,
|
||||
) -> u64 {
|
||||
let reference_images = if reference_image_count == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(ImageListRef::MediaFileTokens(
|
||||
(0..reference_image_count)
|
||||
.map(|i| MediaFileToken::new(format!("m_test_{i}")))
|
||||
.collect(),
|
||||
))
|
||||
};
|
||||
let b = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::MinimaxH3,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("test".to_string()),
|
||||
duration_seconds,
|
||||
resolution,
|
||||
reference_images,
|
||||
..Default::default()
|
||||
};
|
||||
let state = build_artcraft_minimax_h3_state(b).unwrap();
|
||||
ArtcraftMinimaxH3CostState::from_request(&state).estimate_cost().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::providers::artcraft::request_common::send_artcraft_omni_video_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftMinimaxH3RequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftMinimaxH3RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_video_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use test_data::web::image_media_tokens::JUNO_AT_LAKE_PRODUCTION_MEDIA_TOKEN;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // sends a real generation to the Artcraft backend, incurs cost
|
||||
async fn text_to_video() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("A red kite soaring over coastal cliffs, cinematic.".to_string()),
|
||||
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
|
||||
..artcraft_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // sends a real generation to the Artcraft backend, incurs cost
|
||||
async fn keyframe_start_frame() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("The dog stands up and walks toward the lake.".to_string()),
|
||||
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new(JUNO_AT_LAKE_PRODUCTION_MEDIA_TOKEN.to_string()))),
|
||||
..artcraft_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // sends a real generation to the Artcraft backend, incurs cost
|
||||
async fn reference_images_to_video() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("Image 1 is the dog. The dog explores a mossy forest clearing.".to_string()),
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![MediaFileToken::new(JUNO_AT_LAKE_PRODUCTION_MEDIA_TOKEN.to_string())])),
|
||||
..artcraft_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn artcraft_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::MinimaxH3,
|
||||
provider: RouterProvider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_artcraft_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
|
||||
.expect("Failed to read artcraft cookies");
|
||||
let cookies = cookies.trim().to_string();
|
||||
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
|
||||
.expect("Failed to parse cookies")
|
||||
.expect("No credentials found");
|
||||
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
|
||||
let client = get_artcraft_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
VideoGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Artcraft skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateVideoResponse::Artcraft(p) => {
|
||||
println!("inference_job_token={:?}", p.inference_job_token);
|
||||
println!("all_inference_job_tokens={:?}", p.all_inference_job_tokens);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
+1
@@ -12,6 +12,7 @@ pub mod kling_2_5_turbo_pro;
|
||||
pub mod kling_2_6_pro;
|
||||
pub mod kling_3p0_pro;
|
||||
pub mod kling_3p0_standard;
|
||||
pub mod minimax_h3;
|
||||
pub mod preview_model;
|
||||
pub mod preview_model_fast;
|
||||
pub(crate) mod request_common;
|
||||
|
||||
+543
@@ -0,0 +1,543 @@
|
||||
use fal_client::requests::api::video::image::minimax_h3::api::MinimaxH3ImageToVideoRequest;
|
||||
use fal_client::requests::api::video::reference::minimax_h3::api::{
|
||||
MinimaxH3ReferenceToVideoAspectRatio, MinimaxH3ReferenceToVideoRequest,
|
||||
};
|
||||
use fal_client::requests::api::video::text::minimax_h3::api::{
|
||||
MinimaxH3Resolution, MinimaxH3TextToVideoAspectRatio, MinimaxH3TextToVideoRequest,
|
||||
};
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
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;
|
||||
use crate::generate::generate_video::providers::fal::kling_1_6_pro::build::optional_url;
|
||||
use crate::generate::generate_video::providers::fal::minimax_h3::request::{
|
||||
FalMinimaxH3Mode, FalMinimaxH3RequestState,
|
||||
};
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
const MAX_REFERENCE_IMAGES: usize = 9;
|
||||
const MAX_REFERENCE_VIDEOS: usize = 3;
|
||||
const MAX_REFERENCE_AUDIO: usize = 3;
|
||||
const MAX_REFERENCE_FILES: usize = 12;
|
||||
const MIN_DURATION_SECONDS: u16 = 5;
|
||||
const MAX_DURATION_SECONDS: u16 = 15;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum PlanAspectRatio {
|
||||
TwentyOneByNine,
|
||||
SixteenByNine,
|
||||
FourByThree,
|
||||
Square,
|
||||
ThreeByFour,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
pub fn build_fal_minimax_h3(
|
||||
builder: GenerateVideoRequestBuilder,
|
||||
) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let state = build_fal_minimax_h3_state(builder)?;
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::FalMinimaxH3(state)))
|
||||
}
|
||||
|
||||
pub(crate) fn build_fal_minimax_h3_state(
|
||||
builder: GenerateVideoRequestBuilder,
|
||||
) -> Result<FalMinimaxH3RequestState, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
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 reference_image_urls = resolve_image_urls(builder.reference_images.clone())?;
|
||||
let reference_video_urls = resolve_video_urls(builder.reference_videos.clone())?;
|
||||
let reference_audio_urls = resolve_audio_urls(builder.reference_audio.clone())?;
|
||||
let start_image_url = optional_url(builder.start_frame.clone())?;
|
||||
let end_image_url = optional_url(builder.end_frame.clone())?;
|
||||
|
||||
let has_references = !reference_image_urls.is_empty()
|
||||
|| !reference_video_urls.is_empty()
|
||||
|| !reference_audio_urls.is_empty();
|
||||
|
||||
// Dispatch: any reference asset → reference-to-video; start_frame → image; else → text.
|
||||
let mode = if has_references {
|
||||
if start_image_url.is_some() || end_image_url.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "reference_images",
|
||||
value: "MiniMax H3 reference-to-video cannot also accept a start_frame or end_frame".to_string(),
|
||||
}));
|
||||
}
|
||||
check_reference_counts(
|
||||
reference_image_urls.len(),
|
||||
reference_video_urls.len(),
|
||||
reference_audio_urls.len(),
|
||||
)?;
|
||||
FalMinimaxH3Mode::ReferenceToVideo(MinimaxH3ReferenceToVideoRequest {
|
||||
prompt,
|
||||
reference_image_urls: non_empty(reference_image_urls),
|
||||
reference_video_urls: non_empty(reference_video_urls),
|
||||
reference_audio_urls: non_empty(reference_audio_urls),
|
||||
duration,
|
||||
resolution,
|
||||
aspect_ratio: aspect_ratio.map(to_reference_aspect_ratio),
|
||||
})
|
||||
} else if let Some(image_url) = start_image_url {
|
||||
// Image-to-video has no aspect_ratio input — the output follows the start
|
||||
// frame — so any requested aspect ratio is silently dropped here.
|
||||
FalMinimaxH3Mode::ImageToVideo(MinimaxH3ImageToVideoRequest {
|
||||
prompt,
|
||||
image_url,
|
||||
end_image_url,
|
||||
duration,
|
||||
resolution,
|
||||
})
|
||||
} else {
|
||||
if end_image_url.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "end_frame",
|
||||
value: "MiniMax H3 requires a start_frame when end_frame is provided".to_string(),
|
||||
}));
|
||||
}
|
||||
FalMinimaxH3Mode::TextToVideo(MinimaxH3TextToVideoRequest {
|
||||
prompt,
|
||||
duration,
|
||||
resolution,
|
||||
aspect_ratio: aspect_ratio.map(to_t2v_aspect_ratio),
|
||||
})
|
||||
};
|
||||
|
||||
Ok(FalMinimaxH3RequestState { mode })
|
||||
}
|
||||
|
||||
// ── Input helpers ──
|
||||
|
||||
/// fal's reference limits: at most 9 images, 3 videos, 3 audio clips, and 12
|
||||
/// files overall; audio cannot be the only reference input.
|
||||
fn check_reference_counts(
|
||||
image_count: usize,
|
||||
video_count: usize,
|
||||
audio_count: usize,
|
||||
) -> Result<(), ArtcraftRouterError> {
|
||||
if image_count > MAX_REFERENCE_IMAGES {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "reference_images",
|
||||
value: format!("MiniMax H3 supports at most {MAX_REFERENCE_IMAGES} reference images, got {image_count}"),
|
||||
}));
|
||||
}
|
||||
if video_count > MAX_REFERENCE_VIDEOS {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "reference_videos",
|
||||
value: format!("MiniMax H3 supports at most {MAX_REFERENCE_VIDEOS} reference videos, got {video_count}"),
|
||||
}));
|
||||
}
|
||||
if audio_count > MAX_REFERENCE_AUDIO {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "reference_audio",
|
||||
value: format!("MiniMax H3 supports at most {MAX_REFERENCE_AUDIO} reference audio clips, got {audio_count}"),
|
||||
}));
|
||||
}
|
||||
let total = image_count + video_count + audio_count;
|
||||
if total > MAX_REFERENCE_FILES {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "reference_images",
|
||||
value: format!("MiniMax H3 supports at most {MAX_REFERENCE_FILES} reference files overall, got {total}"),
|
||||
}));
|
||||
}
|
||||
if audio_count > 0 && image_count == 0 && video_count == 0 {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "reference_audio",
|
||||
value: "MiniMax H3 requires at least one reference image or video alongside reference audio".to_string(),
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_image_urls(
|
||||
reference_images: Option<ImageListRef>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match reference_images {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_video_urls(
|
||||
reference_videos: Option<VideoListRef>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match reference_videos {
|
||||
None => Ok(vec![]),
|
||||
Some(VideoListRef::Urls(urls)) => Ok(urls),
|
||||
Some(VideoListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_audio_urls(
|
||||
reference_audio: Option<AudioListRef>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match reference_audio {
|
||||
None => Ok(vec![]),
|
||||
Some(AudioListRef::Urls(urls)) => Ok(urls),
|
||||
Some(AudioListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn non_empty(urls: Vec<String>) -> Option<Vec<String>> {
|
||||
if urls.is_empty() { None } else { Some(urls) }
|
||||
}
|
||||
|
||||
// ── Plan helpers ──
|
||||
|
||||
/// `None` (including the `Auto*` ratios) leaves the field unset so fal's
|
||||
/// per-mode default applies: 16:9 for text-to-video, adaptive for
|
||||
/// reference-to-video.
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<RouterAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<PlanAspectRatio>, ArtcraftRouterError> {
|
||||
use PlanAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(RouterAspectRatio::Auto)
|
||||
| Some(RouterAspectRatio::Auto2k)
|
||||
| Some(RouterAspectRatio::Auto3k)
|
||||
| Some(RouterAspectRatio::Auto4k) => Ok(None),
|
||||
|
||||
Some(RouterAspectRatio::WideTwentyOneByNine) => Ok(Some(Ar::TwentyOneByNine)),
|
||||
Some(RouterAspectRatio::WideSixteenByNine) | Some(RouterAspectRatio::Wide) => Ok(Some(Ar::SixteenByNine)),
|
||||
Some(RouterAspectRatio::TallNineBySixteen) | Some(RouterAspectRatio::Tall) => Ok(Some(Ar::NineBySixteen)),
|
||||
Some(RouterAspectRatio::WideFourByThree) => Ok(Some(Ar::FourByThree)),
|
||||
Some(RouterAspectRatio::TallThreeByFour) => Ok(Some(Ar::ThreeByFour)),
|
||||
Some(RouterAspectRatio::Square) | Some(RouterAspectRatio::SquareHd) => Ok(Some(Ar::Square)),
|
||||
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", other),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(Ar::SixteenByNine)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// MiniMax H3 offers exactly two output sizes: 768P and 2K.
|
||||
fn plan_resolution(
|
||||
resolution: Option<RouterResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<MinimaxH3Resolution>, ArtcraftRouterError> {
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
|
||||
// 480p/720p round up to 768P, the smallest size H3 offers.
|
||||
Some(RouterResolution::FourEightyP)
|
||||
| Some(RouterResolution::SevenTwentyP) => Ok(Some(MinimaxH3Resolution::SevenSixtyEightP)),
|
||||
Some(RouterResolution::TenEightyP)
|
||||
| Some(RouterResolution::TwoK) => Ok(Some(MinimaxH3Resolution::TwoK)),
|
||||
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", other),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(MinimaxH3Resolution::TwoK)),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(MinimaxH3Resolution::SevenSixtyEightP)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u8>, ArtcraftRouterError> {
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(seconds) if (MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) => {
|
||||
Ok(Some(seconds as u8))
|
||||
}
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", other),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(other.clamp(MIN_DURATION_SECONDS, MAX_DURATION_SECONDS) as u8))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── Leaf converters ──
|
||||
|
||||
fn to_t2v_aspect_ratio(a: PlanAspectRatio) -> MinimaxH3TextToVideoAspectRatio {
|
||||
match a {
|
||||
PlanAspectRatio::TwentyOneByNine => MinimaxH3TextToVideoAspectRatio::TwentyOneByNine,
|
||||
PlanAspectRatio::SixteenByNine => MinimaxH3TextToVideoAspectRatio::SixteenByNine,
|
||||
PlanAspectRatio::FourByThree => MinimaxH3TextToVideoAspectRatio::FourByThree,
|
||||
PlanAspectRatio::Square => MinimaxH3TextToVideoAspectRatio::Square,
|
||||
PlanAspectRatio::ThreeByFour => MinimaxH3TextToVideoAspectRatio::ThreeByFour,
|
||||
PlanAspectRatio::NineBySixteen => MinimaxH3TextToVideoAspectRatio::NineBySixteen,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_reference_aspect_ratio(a: PlanAspectRatio) -> MinimaxH3ReferenceToVideoAspectRatio {
|
||||
match a {
|
||||
PlanAspectRatio::TwentyOneByNine => MinimaxH3ReferenceToVideoAspectRatio::TwentyOneByNine,
|
||||
PlanAspectRatio::SixteenByNine => MinimaxH3ReferenceToVideoAspectRatio::SixteenByNine,
|
||||
PlanAspectRatio::FourByThree => MinimaxH3ReferenceToVideoAspectRatio::FourByThree,
|
||||
PlanAspectRatio::Square => MinimaxH3ReferenceToVideoAspectRatio::Square,
|
||||
PlanAspectRatio::ThreeByFour => MinimaxH3ReferenceToVideoAspectRatio::ThreeByFour,
|
||||
PlanAspectRatio::NineBySixteen => MinimaxH3ReferenceToVideoAspectRatio::NineBySixteen,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
|
||||
use super::*;
|
||||
|
||||
const START_URL: &str = "https://example.com/start.png";
|
||||
const END_URL: &str = "https://example.com/end.png";
|
||||
|
||||
mod dispatch_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn no_inputs_picks_t2v() {
|
||||
let state = build_fal_minimax_h3_state(base_builder()).expect("build");
|
||||
assert!(matches!(state.mode, FalMinimaxH3Mode::TextToVideo(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_frame_picks_i2v() {
|
||||
let mut b = base_builder();
|
||||
b.start_frame = Some(ImageRef::Url(START_URL.to_string()));
|
||||
let state = build_fal_minimax_h3_state(b).expect("build");
|
||||
assert!(matches!(state.mode, FalMinimaxH3Mode::ImageToVideo(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_and_end_frame_picks_i2v_with_end_image_url() {
|
||||
let mut b = base_builder();
|
||||
b.start_frame = Some(ImageRef::Url(START_URL.to_string()));
|
||||
b.end_frame = Some(ImageRef::Url(END_URL.to_string()));
|
||||
let state = build_fal_minimax_h3_state(b).expect("build");
|
||||
let FalMinimaxH3Mode::ImageToVideo(req) = state.mode else {
|
||||
panic!("expected ImageToVideo");
|
||||
};
|
||||
assert_eq!(req.image_url, START_URL);
|
||||
assert_eq!(req.end_image_url.as_deref(), Some(END_URL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_frame_without_start_frame_errors() {
|
||||
let mut b = base_builder();
|
||||
b.end_frame = Some(ImageRef::Url(END_URL.to_string()));
|
||||
assert!(build_fal_minimax_h3_state(b).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_images_pick_reference_to_video() {
|
||||
let state = build_fal_minimax_h3_state(builder_with_reference_images(3)).expect("build");
|
||||
let FalMinimaxH3Mode::ReferenceToVideo(req) = state.mode else {
|
||||
panic!("expected ReferenceToVideo");
|
||||
};
|
||||
assert_eq!(req.reference_image_urls.map(|urls| urls.len()), Some(3));
|
||||
assert_eq!(req.reference_video_urls, None);
|
||||
assert_eq!(req.reference_audio_urls, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_videos_pick_reference_to_video() {
|
||||
let mut b = base_builder();
|
||||
b.reference_videos = Some(VideoListRef::Urls(vec!["https://example.com/a.mp4".to_string()]));
|
||||
let state = build_fal_minimax_h3_state(b).expect("build");
|
||||
let FalMinimaxH3Mode::ReferenceToVideo(req) = state.mode else {
|
||||
panic!("expected ReferenceToVideo");
|
||||
};
|
||||
assert_eq!(req.reference_video_urls.map(|urls| urls.len()), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_reference_images_error() {
|
||||
assert!(build_fal_minimax_h3_state(builder_with_reference_images(10)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_reference_videos_error() {
|
||||
let mut b = base_builder();
|
||||
b.reference_videos = Some(VideoListRef::Urls(
|
||||
(0..4).map(|i| format!("https://example.com/v-{i}.mp4")).collect(),
|
||||
));
|
||||
assert!(build_fal_minimax_h3_state(b).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thirteen_reference_files_error() {
|
||||
// 9 images + 3 videos + 1 audio = 13 > 12 total.
|
||||
let mut b = builder_with_reference_images(9);
|
||||
b.reference_videos = Some(VideoListRef::Urls(
|
||||
(0..3).map(|i| format!("https://example.com/v-{i}.mp4")).collect(),
|
||||
));
|
||||
b.reference_audio = Some(AudioListRef::Urls(vec!["https://example.com/a.mp3".to_string()]));
|
||||
assert!(build_fal_minimax_h3_state(b).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_only_reference_errors() {
|
||||
let mut b = base_builder();
|
||||
b.reference_audio = Some(AudioListRef::Urls(vec!["https://example.com/a.mp3".to_string()]));
|
||||
assert!(build_fal_minimax_h3_state(b).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_images_with_start_frame_error() {
|
||||
let mut b = builder_with_reference_images(2);
|
||||
b.start_frame = Some(ImageRef::Url(START_URL.to_string()));
|
||||
assert!(build_fal_minimax_h3_state(b).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod plan_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn duration_20_errors_under_error_out() {
|
||||
let mut b = base_builder();
|
||||
b.duration_seconds = Some(20);
|
||||
b.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::ErrorOut;
|
||||
assert!(build_fal_minimax_h3_state(b).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_20_clamps_to_15_under_pay_less() {
|
||||
let mut b = base_builder();
|
||||
b.duration_seconds = Some(20);
|
||||
b.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::PayLessDowngrade;
|
||||
let state = build_fal_minimax_h3_state(b).expect("build");
|
||||
let FalMinimaxH3Mode::TextToVideo(req) = state.mode else {
|
||||
panic!("expected TextToVideo");
|
||||
};
|
||||
assert_eq!(req.duration, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_3_clamps_to_5_under_pay_less() {
|
||||
let mut b = base_builder();
|
||||
b.duration_seconds = Some(3);
|
||||
b.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::PayLessDowngrade;
|
||||
let state = build_fal_minimax_h3_state(b).expect("build");
|
||||
let FalMinimaxH3Mode::TextToVideo(req) = state.mode else {
|
||||
panic!("expected TextToVideo");
|
||||
};
|
||||
assert_eq!(req.duration, Some(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_480p_and_720p_map_to_768p() {
|
||||
for res in [RouterResolution::FourEightyP, RouterResolution::SevenTwentyP] {
|
||||
let mut b = base_builder();
|
||||
b.resolution = Some(res);
|
||||
let state = build_fal_minimax_h3_state(b).expect("build");
|
||||
let FalMinimaxH3Mode::TextToVideo(req) = state.mode else {
|
||||
panic!("expected TextToVideo");
|
||||
};
|
||||
assert_eq!(req.resolution, Some(MinimaxH3Resolution::SevenSixtyEightP), "res={res:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_1080p_and_2k_map_to_2k() {
|
||||
for res in [RouterResolution::TenEightyP, RouterResolution::TwoK] {
|
||||
let mut b = base_builder();
|
||||
b.resolution = Some(res);
|
||||
let state = build_fal_minimax_h3_state(b).expect("build");
|
||||
let FalMinimaxH3Mode::TextToVideo(req) = state.mode else {
|
||||
panic!("expected TextToVideo");
|
||||
};
|
||||
assert_eq!(req.resolution, Some(MinimaxH3Resolution::TwoK), "res={res:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_4k_errors_under_error_out() {
|
||||
let mut b = base_builder();
|
||||
b.resolution = Some(RouterResolution::FourK);
|
||||
b.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::ErrorOut;
|
||||
assert!(build_fal_minimax_h3_state(b).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twenty_one_by_nine_is_supported() {
|
||||
let mut b = base_builder();
|
||||
b.aspect_ratio = Some(RouterAspectRatio::WideTwentyOneByNine);
|
||||
b.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::ErrorOut;
|
||||
let state = build_fal_minimax_h3_state(b).expect("build");
|
||||
let FalMinimaxH3Mode::TextToVideo(req) = state.mode else {
|
||||
panic!("expected TextToVideo");
|
||||
};
|
||||
assert_eq!(req.aspect_ratio, Some(MinimaxH3TextToVideoAspectRatio::TwentyOneByNine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_aspect_ratio_is_left_unset() {
|
||||
let mut b = base_builder();
|
||||
b.aspect_ratio = Some(RouterAspectRatio::Auto);
|
||||
let state = build_fal_minimax_h3_state(b).expect("build");
|
||||
let FalMinimaxH3Mode::TextToVideo(req) = state.mode else {
|
||||
panic!("expected TextToVideo");
|
||||
};
|
||||
assert_eq!(req.aspect_ratio, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_aspect_ratio_is_silently_dropped() {
|
||||
let mut b = base_builder();
|
||||
b.start_frame = Some(ImageRef::Url(START_URL.to_string()));
|
||||
b.aspect_ratio = Some(RouterAspectRatio::TallNineBySixteen);
|
||||
b.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::ErrorOut;
|
||||
// Image-to-video has no aspect_ratio field; the request still builds.
|
||||
let state = build_fal_minimax_h3_state(b).expect("build");
|
||||
assert!(matches!(state.mode, FalMinimaxH3Mode::ImageToVideo(_)));
|
||||
}
|
||||
}
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::MinimaxH3,
|
||||
provider: RouterProvider::Fal,
|
||||
prompt: Some("test".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn builder_with_reference_images(count: usize) -> GenerateVideoRequestBuilder {
|
||||
let urls = (0..count)
|
||||
.map(|i| format!("https://example.com/ref-{}.png", i))
|
||||
.collect();
|
||||
let mut b = base_builder();
|
||||
b.reference_images = Some(ImageListRef::Urls(urls));
|
||||
b
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_video::providers::fal::minimax_h3::request::{
|
||||
FalMinimaxH3Mode, FalMinimaxH3RequestState,
|
||||
};
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalMinimaxH3CostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalMinimaxH3CostState {
|
||||
pub fn from_request(request: &FalMinimaxH3RequestState) -> Self {
|
||||
// Cost math is owned by fal_client's per-endpoint
|
||||
// `FalRequestCostCalculator` implementations. The router state just
|
||||
// forwards the result so router cost ≡ fal_client cost by construction.
|
||||
let cost_in_usd_cents = match &request.mode {
|
||||
FalMinimaxH3Mode::TextToVideo(req) => req.calculate_cost_in_cents(),
|
||||
FalMinimaxH3Mode::ImageToVideo(req) => req.calculate_cost_in_cents(),
|
||||
FalMinimaxH3Mode::ReferenceToVideo(req) => req.calculate_cost_in_cents(),
|
||||
};
|
||||
Self { cost_in_usd_cents }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::fal::minimax_h3::build::build_fal_minimax_h3_state;
|
||||
|
||||
use super::*;
|
||||
|
||||
// Pricing (from fal_client's minimax_h3 cost modules):
|
||||
// 768P: $0.16/sec → 5s = 80¢
|
||||
// 2K: $0.26/sec → 5s = 130¢, 10s = 260¢
|
||||
// Reference-to-video additionally bills 8¢ per reference image beyond the
|
||||
// first 5. fal defaults when unset: duration = 5s, resolution = 2K.
|
||||
|
||||
#[test]
|
||||
fn t2v_default_settings_is_130() {
|
||||
assert_eq!(cost_cents(base_builder(None, None)), 130);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t2v_5s_768p_is_80() {
|
||||
// RouterResolution::SevenTwentyP maps to H3's 768P.
|
||||
assert_eq!(cost_cents(base_builder(Some(5), Some(RouterResolution::SevenTwentyP))), 80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t2v_10s_2k_is_260() {
|
||||
assert_eq!(cost_cents(base_builder(Some(10), Some(RouterResolution::TenEightyP))), 260);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_5s_768p_is_80() {
|
||||
let mut b = base_builder(Some(5), Some(RouterResolution::FourEightyP));
|
||||
b.start_frame = Some(ImageRef::Url("https://example.com/start.png".to_string()));
|
||||
assert_eq!(cost_cents(b), 80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_five_images_5s_2k_is_130() {
|
||||
assert_eq!(cost_cents(builder_with_references(5, Some(RouterResolution::TenEightyP))), 130);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_nine_images_5s_2k_is_162() {
|
||||
// 130¢ video + 4 extra images × 8¢ = 162¢.
|
||||
assert_eq!(cost_cents(builder_with_references(9, Some(RouterResolution::TenEightyP))), 162);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn estimate_cost_forwards_cents() {
|
||||
let estimate = FalMinimaxH3CostState { cost_in_usd_cents: 130 }.estimate_cost();
|
||||
assert_eq!(estimate.cost_in_usd_cents, Some(130));
|
||||
assert_eq!(estimate.cost_in_credits, Some(130));
|
||||
assert!(!estimate.is_free);
|
||||
}
|
||||
|
||||
fn base_builder(
|
||||
duration_seconds: Option<u16>,
|
||||
resolution: Option<RouterResolution>,
|
||||
) -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::MinimaxH3,
|
||||
provider: RouterProvider::Fal,
|
||||
prompt: Some("test".to_string()),
|
||||
duration_seconds,
|
||||
resolution,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn builder_with_references(
|
||||
image_count: usize,
|
||||
resolution: Option<RouterResolution>,
|
||||
) -> GenerateVideoRequestBuilder {
|
||||
let urls = (0..image_count)
|
||||
.map(|i| format!("https://example.com/ref-{}.png", i))
|
||||
.collect();
|
||||
let mut b = base_builder(Some(5), resolution);
|
||||
b.reference_images = Some(ImageListRef::Urls(urls));
|
||||
b
|
||||
}
|
||||
|
||||
fn cost_cents(builder: GenerateVideoRequestBuilder) -> u64 {
|
||||
let state = build_fal_minimax_h3_state(builder).expect("build");
|
||||
FalMinimaxH3CostState::from_request(&state).cost_in_usd_cents
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::api::video::image::minimax_h3::api::MinimaxH3ImageToVideoRequest;
|
||||
use fal_client::requests::api::video::reference::minimax_h3::api::MinimaxH3ReferenceToVideoRequest;
|
||||
use fal_client::requests::api::video::text::minimax_h3::api::MinimaxH3TextToVideoRequest;
|
||||
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
FalVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FalMinimaxH3Mode {
|
||||
TextToVideo(MinimaxH3TextToVideoRequest),
|
||||
ImageToVideo(MinimaxH3ImageToVideoRequest),
|
||||
ReferenceToVideo(MinimaxH3ReferenceToVideoRequest),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalMinimaxH3RequestState {
|
||||
pub mode: FalMinimaxH3Mode,
|
||||
}
|
||||
|
||||
impl FalMinimaxH3RequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
match &self.mode {
|
||||
FalMinimaxH3Mode::TextToVideo(request) => send_request(request, client).await,
|
||||
FalMinimaxH3Mode::ImageToVideo(request) => send_request(request, client).await,
|
||||
FalMinimaxH3Mode::ReferenceToVideo(request) => send_request(request, client).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
async fn send_request<T>(request: &T, client: &RouterFalClient) -> Result<GenerateVideoResponse, ArtcraftRouterError>
|
||||
where
|
||||
T: FalEndpoint + Clone + Debug + Send + Sync + 'static,
|
||||
{
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
|
||||
let payload = send_fal_request(request, client).await?;
|
||||
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
|
||||
request_id: payload.request_id,
|
||||
gateway_request_id: payload.gateway_request_id,
|
||||
maybe_status_url: payload.status_url,
|
||||
maybe_response_url: payload.response_url,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}))
|
||||
}
|
||||
|
||||
struct FalResponseIds {
|
||||
request_id: Option<String>,
|
||||
gateway_request_id: Option<String>,
|
||||
status_url: Option<String>,
|
||||
response_url: Option<String>,
|
||||
}
|
||||
|
||||
async fn send_fal_request<T: FalEndpoint>(
|
||||
request: &T,
|
||||
client: &RouterFalClient,
|
||||
) -> Result<FalResponseIds, ArtcraftRouterError> {
|
||||
if let Some(webhook_url) = &client.webhook_url {
|
||||
let response = request.send_webhook_request(&client.api_key, webhook_url).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: response.request_id,
|
||||
gateway_request_id: response.gateway_request_id,
|
||||
status_url: None,
|
||||
response_url: None,
|
||||
})
|
||||
} else {
|
||||
let response = request.send_queue_request(&client.api_key).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: Some(response.request_id),
|
||||
gateway_request_id: None,
|
||||
status_url: Some(response.status_url),
|
||||
response_url: Some(response.response_url),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use fal_client::requests::api::video::text::minimax_h3::api::{
|
||||
MinimaxH3Resolution, MinimaxH3TextToVideoAspectRatio,
|
||||
};
|
||||
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_text_to_video_webhook() {
|
||||
let state = FalMinimaxH3RequestState {
|
||||
mode: FalMinimaxH3Mode::TextToVideo(MinimaxH3TextToVideoRequest {
|
||||
prompt: "a calm lake at sunrise".to_string(),
|
||||
duration: Some(5),
|
||||
resolution: Some(MinimaxH3Resolution::SevenSixtyEightP),
|
||||
aspect_ratio: Some(MinimaxH3TextToVideoAspectRatio::SixteenByNine),
|
||||
}),
|
||||
};
|
||||
let response = state.send(&client_with_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_image_to_video_webhook() {
|
||||
let state = FalMinimaxH3RequestState {
|
||||
mode: FalMinimaxH3Mode::ImageToVideo(MinimaxH3ImageToVideoRequest {
|
||||
prompt: "the lake comes alive".to_string(),
|
||||
image_url: JUNO_AT_LAKE_IMAGE_URL.to_string(),
|
||||
end_image_url: None,
|
||||
duration: Some(5),
|
||||
resolution: Some(MinimaxH3Resolution::SevenSixtyEightP),
|
||||
}),
|
||||
};
|
||||
let response = state.send(&client_with_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn read_fal_api_key() -> FalApiKey {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
FalApiKey::from_str(secret.trim())
|
||||
}
|
||||
|
||||
fn client_with_webhook() -> RouterFalClient {
|
||||
RouterFalClient::new_with_webhook(
|
||||
read_fal_api_key(),
|
||||
"https://example.com/fal-webhook-test".to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+1
@@ -5,6 +5,7 @@ pub mod kling_2_5_turbo_pro;
|
||||
pub mod kling_2_6_pro;
|
||||
pub mod kling_3p0_pro;
|
||||
pub mod kling_3p0_standard;
|
||||
pub mod minimax_h3;
|
||||
pub mod seedance_1p0_lite;
|
||||
pub mod seedance_1p5_pro;
|
||||
pub mod sora_2;
|
||||
|
||||
+18
@@ -23,6 +23,8 @@ use crate::generate::generate_video::providers::artcraft::kling_3p0_pro::cost::A
|
||||
use crate::generate::generate_video::providers::artcraft::kling_3p0_pro::request::ArtcraftKling3p0ProRequestState;
|
||||
use crate::generate::generate_video::providers::artcraft::kling_3p0_standard::cost::ArtcraftKling3p0StandardCostState;
|
||||
use crate::generate::generate_video::providers::artcraft::kling_3p0_standard::request::ArtcraftKling3p0StandardRequestState;
|
||||
use crate::generate::generate_video::providers::artcraft::minimax_h3::cost::ArtcraftMinimaxH3CostState;
|
||||
use crate::generate::generate_video::providers::artcraft::minimax_h3::request::ArtcraftMinimaxH3RequestState;
|
||||
use crate::generate::generate_video::providers::artcraft::preview_model::cost::ArtcraftPreviewModelCostState;
|
||||
use crate::generate::generate_video::providers::artcraft::preview_model::request::ArtcraftPreviewModelRequestState;
|
||||
use crate::generate::generate_video::providers::artcraft::preview_model_fast::cost::ArtcraftPreviewModelFastCostState;
|
||||
@@ -89,6 +91,8 @@ use crate::generate::generate_video::providers::fal::kling_3p0_pro::cost::FalKli
|
||||
use crate::generate::generate_video::providers::fal::kling_3p0_pro::request::FalKling3p0ProRequestState;
|
||||
use crate::generate::generate_video::providers::fal::kling_3p0_standard::cost::FalKling3p0StandardCostState;
|
||||
use crate::generate::generate_video::providers::fal::kling_3p0_standard::request::FalKling3p0StandardRequestState;
|
||||
use crate::generate::generate_video::providers::fal::minimax_h3::cost::FalMinimaxH3CostState;
|
||||
use crate::generate::generate_video::providers::fal::minimax_h3::request::FalMinimaxH3RequestState;
|
||||
use crate::generate::generate_video::providers::fal::seedance_1p0_lite::cost::FalSeedance10LiteCostState;
|
||||
use crate::generate::generate_video::providers::fal::seedance_1p0_lite::request::FalSeedance10LiteRequestState;
|
||||
use crate::generate::generate_video::providers::fal::sora_2::cost::FalSora2CostState;
|
||||
@@ -144,6 +148,7 @@ pub enum VideoGenerationRequest {
|
||||
ArtcraftKling2p6Pro(ArtcraftKling2p6ProRequestState),
|
||||
ArtcraftKling3p0Pro(ArtcraftKling3p0ProRequestState),
|
||||
ArtcraftKling3p0Standard(ArtcraftKling3p0StandardRequestState),
|
||||
ArtcraftMinimaxH3(ArtcraftMinimaxH3RequestState),
|
||||
ArtcraftPreviewModel(ArtcraftPreviewModelRequestState),
|
||||
ArtcraftPreviewModelFast(ArtcraftPreviewModelFastRequestState),
|
||||
ArtcraftSeedance10Lite(ArtcraftSeedance10LiteRequestState),
|
||||
@@ -177,6 +182,7 @@ pub enum VideoGenerationRequest {
|
||||
FalKling2p6Pro(FalKling2p6ProRequestState),
|
||||
FalKling3p0Pro(FalKling3p0ProRequestState),
|
||||
FalKling3p0Standard(FalKling3p0StandardRequestState),
|
||||
FalMinimaxH3(FalMinimaxH3RequestState),
|
||||
FalSeedance10Lite(FalSeedance10LiteRequestState),
|
||||
FalSeedance1p5Pro(FalSeedance1p5ProRequestState),
|
||||
FalSora2(FalSora2RequestState),
|
||||
@@ -214,6 +220,7 @@ impl VideoGenerationRequest {
|
||||
Self::ArtcraftKling2p6Pro(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftKling3p0Pro(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftKling3p0Standard(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftMinimaxH3(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftPreviewModel(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftPreviewModelFast(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftSeedance10Lite(_) => RouterProvider::Artcraft,
|
||||
@@ -247,6 +254,7 @@ impl VideoGenerationRequest {
|
||||
Self::FalKling2p6Pro(_) => RouterProvider::Fal,
|
||||
Self::FalKling3p0Pro(_) => RouterProvider::Fal,
|
||||
Self::FalKling3p0Standard(_) => RouterProvider::Fal,
|
||||
Self::FalMinimaxH3(_) => RouterProvider::Fal,
|
||||
Self::FalSeedance10Lite(_) => RouterProvider::Fal,
|
||||
Self::FalSeedance1p5Pro(_) => RouterProvider::Fal,
|
||||
Self::FalSora2(_) => RouterProvider::Fal,
|
||||
@@ -284,6 +292,7 @@ impl VideoGenerationRequest {
|
||||
VideoGenerationRequest::ArtcraftKling2p6Pro(request) => Ok(ArtcraftKling2p6ProCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftKling3p0Pro(request) => Ok(ArtcraftKling3p0ProCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftKling3p0Standard(request) => Ok(ArtcraftKling3p0StandardCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftMinimaxH3(request) => Ok(ArtcraftMinimaxH3CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftPreviewModel(request) => Ok(ArtcraftPreviewModelCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftPreviewModelFast(request) => Ok(ArtcraftPreviewModelFastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance10Lite(request) => Ok(ArtcraftSeedance10LiteCostState::from_request(request).estimate_cost()),
|
||||
@@ -317,6 +326,7 @@ impl VideoGenerationRequest {
|
||||
VideoGenerationRequest::FalKling2p6Pro(request) => Ok(FalKling2p6ProCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::FalKling3p0Pro(request) => Ok(FalKling3p0ProCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::FalKling3p0Standard(request) => Ok(FalKling3p0StandardCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::FalMinimaxH3(request) => Ok(FalMinimaxH3CostState::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::FalSora2(request) => Ok(FalSora2CostState::from_request(request).estimate_cost()),
|
||||
@@ -385,6 +395,10 @@ impl VideoGenerationRequest {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftMinimaxH3(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftPreviewModel(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
@@ -517,6 +531,10 @@ impl VideoGenerationRequest {
|
||||
let client_ref = client.get_fal_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::FalMinimaxH3(request) => {
|
||||
let client_ref = client.get_fal_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::FalSeedance10Lite(request) => {
|
||||
let client_ref = client.get_fal_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
|
||||
+1
@@ -61,6 +61,7 @@ fn video_model_to_common_model_type(model: RouterVideoModel) -> Option<CommonMod
|
||||
RouterVideoModel::Seedance2p0 => Some(CommonModelType::Seedance2p0),
|
||||
RouterVideoModel::Seedance2p0Fast => Some(CommonModelType::Seedance2p0Fast),
|
||||
RouterVideoModel::HappyHorse1p0 => Some(CommonModelType::HappyHorse1p0),
|
||||
RouterVideoModel::MinimaxH3 => Some(CommonModelType::MinimaxH3),
|
||||
RouterVideoModel::Sora2 => Some(CommonModelType::Sora2),
|
||||
RouterVideoModel::Sora2Pro => Some(CommonModelType::Sora2Pro),
|
||||
RouterVideoModel::Veo2 => Some(CommonModelType::Veo2),
|
||||
|
||||
@@ -180,6 +180,8 @@ pub enum InferenceModelType {
|
||||
Kling3p0Pro,
|
||||
#[serde(rename = "happy_horse_1p0")]
|
||||
HappyHorse1p0,
|
||||
#[serde(rename = "minimax_h3")]
|
||||
MinimaxH3,
|
||||
#[serde(rename = "seedance_1p0_lite")]
|
||||
Seedance10Lite,
|
||||
#[serde(rename = "seedance_1p0_pro")]
|
||||
@@ -366,6 +368,7 @@ impl InferenceModelType {
|
||||
Self::Kling3p0Standard => "kling_3p0_standard",
|
||||
Self::Kling3p0Pro => "kling_3p0_pro",
|
||||
Self::HappyHorse1p0 => "happy_horse_1p0",
|
||||
Self::MinimaxH3 => "minimax_h3",
|
||||
Self::Seedance10Lite => "seedance_1p0_lite",
|
||||
Self::Seedance10Pro => "seedance_1p0_pro",
|
||||
Self::Seedance1p5Pro => "seedance_1p5_pro",
|
||||
@@ -497,6 +500,7 @@ impl InferenceModelType {
|
||||
"kling_3p0_standard" => Ok(Self::Kling3p0Standard),
|
||||
"kling_3p0_pro" => Ok(Self::Kling3p0Pro),
|
||||
"happy_horse_1p0" => Ok(Self::HappyHorse1p0),
|
||||
"minimax_h3" => Ok(Self::MinimaxH3),
|
||||
"seedance_1p0_lite" => Ok(Self::Seedance10Lite),
|
||||
"seedance_1p0_pro" => Ok(Self::Seedance10Pro),
|
||||
"seedance_1p5_pro" => Ok(Self::Seedance1p5Pro),
|
||||
@@ -632,6 +636,7 @@ impl InferenceModelType {
|
||||
Self::Kling3p0Standard,
|
||||
Self::Kling3p0Pro,
|
||||
Self::HappyHorse1p0,
|
||||
Self::MinimaxH3,
|
||||
Self::Seedance10Lite,
|
||||
Self::Seedance10Pro,
|
||||
Self::Seedance1p5Pro,
|
||||
@@ -750,6 +755,7 @@ impl InferenceModelType {
|
||||
CommonModelType::Kling3p0Standard => Self::Kling3p0Standard,
|
||||
CommonModelType::Kling3p0Pro => Self::Kling3p0Pro,
|
||||
CommonModelType::HappyHorse1p0 => Self::HappyHorse1p0,
|
||||
CommonModelType::MinimaxH3 => Self::MinimaxH3,
|
||||
CommonModelType::Seedance10Lite => Self::Seedance10Lite,
|
||||
CommonModelType::Seedance10Pro => Self::Seedance10Pro,
|
||||
CommonModelType::Seedance1p5Pro => Self::Seedance1p5Pro,
|
||||
@@ -890,6 +896,7 @@ mod tests {
|
||||
assert_serialization(InferenceModelType::Kling3p0Standard, "kling_3p0_standard");
|
||||
assert_serialization(InferenceModelType::Kling3p0Pro, "kling_3p0_pro");
|
||||
assert_serialization(InferenceModelType::HappyHorse1p0, "happy_horse_1p0");
|
||||
assert_serialization(InferenceModelType::MinimaxH3, "minimax_h3");
|
||||
assert_serialization(InferenceModelType::Seedance10Lite, "seedance_1p0_lite");
|
||||
assert_serialization(InferenceModelType::Seedance10Pro, "seedance_1p0_pro");
|
||||
assert_serialization(InferenceModelType::Seedance1p5Pro, "seedance_1p5_pro");
|
||||
@@ -1017,6 +1024,7 @@ mod tests {
|
||||
assert_eq!(InferenceModelType::Kling3p0Standard.to_str(), "kling_3p0_standard");
|
||||
assert_eq!(InferenceModelType::Kling3p0Pro.to_str(), "kling_3p0_pro");
|
||||
assert_eq!(InferenceModelType::HappyHorse1p0.to_str(), "happy_horse_1p0");
|
||||
assert_eq!(InferenceModelType::MinimaxH3.to_str(), "minimax_h3");
|
||||
assert_eq!(InferenceModelType::Seedance10Lite.to_str(), "seedance_1p0_lite");
|
||||
assert_eq!(InferenceModelType::Seedance10Pro.to_str(), "seedance_1p0_pro");
|
||||
assert_eq!(InferenceModelType::Seedance1p5Pro.to_str(), "seedance_1p5_pro");
|
||||
@@ -1146,6 +1154,7 @@ mod tests {
|
||||
assert_eq!(InferenceModelType::from_str("kling_3p0_standard").unwrap(), InferenceModelType::Kling3p0Standard);
|
||||
assert_eq!(InferenceModelType::from_str("kling_3p0_pro").unwrap(), InferenceModelType::Kling3p0Pro);
|
||||
assert_eq!(InferenceModelType::from_str("happy_horse_1p0").unwrap(), InferenceModelType::HappyHorse1p0);
|
||||
assert_eq!(InferenceModelType::from_str("minimax_h3").unwrap(), InferenceModelType::MinimaxH3);
|
||||
assert_eq!(InferenceModelType::from_str("seedance_1p0_lite").unwrap(), InferenceModelType::Seedance10Lite);
|
||||
assert_eq!(InferenceModelType::from_str("seedance_1p0_pro").unwrap(), InferenceModelType::Seedance10Pro);
|
||||
assert_eq!(InferenceModelType::from_str("seedance_1p5_pro").unwrap(), InferenceModelType::Seedance1p5Pro);
|
||||
|
||||
@@ -151,6 +151,8 @@ pub enum CommonModelType {
|
||||
Kling3p0Pro,
|
||||
#[serde(rename = "happy_horse_1p0")]
|
||||
HappyHorse1p0,
|
||||
#[serde(rename = "minimax_h3")]
|
||||
MinimaxH3,
|
||||
#[serde(rename = "seedance_1p0_lite")]
|
||||
Seedance10Lite,
|
||||
#[serde(rename = "seedance_1p0_pro")]
|
||||
@@ -321,6 +323,7 @@ impl CommonModelType {
|
||||
Self::Kling3p0Standard => "kling_3p0_standard",
|
||||
Self::Kling3p0Pro => "kling_3p0_pro",
|
||||
Self::HappyHorse1p0 => "happy_horse_1p0",
|
||||
Self::MinimaxH3 => "minimax_h3",
|
||||
Self::Seedance10Lite => "seedance_1p0_lite",
|
||||
Self::Seedance10Pro => "seedance_1p0_pro",
|
||||
Self::Seedance1p5Pro => "seedance_1p5_pro",
|
||||
@@ -435,6 +438,7 @@ impl CommonModelType {
|
||||
"kling_3p0_standard" => Ok(Self::Kling3p0Standard),
|
||||
"kling_3p0_pro" => Ok(Self::Kling3p0Pro),
|
||||
"happy_horse_1p0" => Ok(Self::HappyHorse1p0),
|
||||
"minimax_h3" => Ok(Self::MinimaxH3),
|
||||
"seedance_1p0_lite" => Ok(Self::Seedance10Lite),
|
||||
"seedance_1p0_pro" => Ok(Self::Seedance10Pro),
|
||||
"seedance_1p5_pro" => Ok(Self::Seedance1p5Pro),
|
||||
@@ -553,6 +557,7 @@ impl CommonModelType {
|
||||
Self::Kling3p0Standard,
|
||||
Self::Kling3p0Pro,
|
||||
Self::HappyHorse1p0,
|
||||
Self::MinimaxH3,
|
||||
Self::Seedance10Lite,
|
||||
Self::Seedance10Pro,
|
||||
Self::Seedance1p5Pro,
|
||||
@@ -668,6 +673,7 @@ impl CommonModelType {
|
||||
Self::Kling3p0Standard => CommonModelClass::Video,
|
||||
Self::Kling3p0Pro => CommonModelClass::Video,
|
||||
Self::HappyHorse1p0 => CommonModelClass::Video,
|
||||
Self::MinimaxH3 => CommonModelClass::Video,
|
||||
Self::Seedance10Lite => CommonModelClass::Video,
|
||||
Self::Seedance10Pro => CommonModelClass::Video,
|
||||
Self::Seedance1p5Pro => CommonModelClass::Video,
|
||||
@@ -792,6 +798,7 @@ mod tests {
|
||||
assert_serialization(CommonModelType::Kling3p0Standard, "kling_3p0_standard");
|
||||
assert_serialization(CommonModelType::Kling3p0Pro, "kling_3p0_pro");
|
||||
assert_serialization(CommonModelType::HappyHorse1p0, "happy_horse_1p0");
|
||||
assert_serialization(CommonModelType::MinimaxH3, "minimax_h3");
|
||||
assert_serialization(CommonModelType::Seedance10Lite, "seedance_1p0_lite");
|
||||
assert_serialization(CommonModelType::Seedance10Pro, "seedance_1p0_pro");
|
||||
assert_serialization(CommonModelType::Seedance1p5Pro, "seedance_1p5_pro");
|
||||
@@ -901,6 +908,7 @@ mod tests {
|
||||
assert_eq!(CommonModelType::Kling3p0Standard.to_str(), "kling_3p0_standard");
|
||||
assert_eq!(CommonModelType::Kling3p0Pro.to_str(), "kling_3p0_pro");
|
||||
assert_eq!(CommonModelType::HappyHorse1p0.to_str(), "happy_horse_1p0");
|
||||
assert_eq!(CommonModelType::MinimaxH3.to_str(), "minimax_h3");
|
||||
assert_eq!(CommonModelType::Seedance10Lite.to_str(), "seedance_1p0_lite");
|
||||
assert_eq!(CommonModelType::Seedance10Pro.to_str(), "seedance_1p0_pro");
|
||||
assert_eq!(CommonModelType::Seedance1p5Pro.to_str(), "seedance_1p5_pro");
|
||||
@@ -1012,6 +1020,7 @@ mod tests {
|
||||
assert_eq!(CommonModelType::from_str("kling_3p0_standard").unwrap(), CommonModelType::Kling3p0Standard);
|
||||
assert_eq!(CommonModelType::from_str("kling_3p0_pro").unwrap(), CommonModelType::Kling3p0Pro);
|
||||
assert_eq!(CommonModelType::from_str("happy_horse_1p0").unwrap(), CommonModelType::HappyHorse1p0);
|
||||
assert_eq!(CommonModelType::from_str("minimax_h3").unwrap(), CommonModelType::MinimaxH3);
|
||||
assert_eq!(CommonModelType::from_str("seedance_1p0_lite").unwrap(), CommonModelType::Seedance10Lite);
|
||||
assert_eq!(CommonModelType::from_str("seedance_1p0_pro").unwrap(), CommonModelType::Seedance10Pro);
|
||||
assert_eq!(CommonModelType::from_str("seedance_1p5_pro").unwrap(), CommonModelType::Seedance1p5Pro);
|
||||
@@ -1071,7 +1080,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = CommonModelType::all_variants();
|
||||
assert_eq!(variants.len(), 99);
|
||||
assert_eq!(variants.len(), 100);
|
||||
// Image models
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Dev));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Schnell));
|
||||
@@ -1123,6 +1132,7 @@ mod tests {
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Kling3p0Standard));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Kling3p0Pro));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::HappyHorse1p0));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::MinimaxH3));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance10Lite));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance10Pro));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance1p5Pro));
|
||||
|
||||
@@ -41,6 +41,9 @@ pub enum CommonVideoModel {
|
||||
#[serde(rename = "happy_horse_1p0")]
|
||||
HappyHorse1p0,
|
||||
|
||||
#[serde(rename = "minimax_h3")]
|
||||
MinimaxH3,
|
||||
|
||||
#[serde(rename = "seedance_1p0_lite")]
|
||||
Seedance10Lite,
|
||||
|
||||
@@ -139,6 +142,7 @@ impl CommonVideoModel {
|
||||
Self::Kling3p0Standard => CommonModelType::Kling3p0Standard,
|
||||
Self::Kling3p0Pro => CommonModelType::Kling3p0Pro,
|
||||
Self::HappyHorse1p0 => CommonModelType::HappyHorse1p0,
|
||||
Self::MinimaxH3 => CommonModelType::MinimaxH3,
|
||||
Self::Seedance10Lite => CommonModelType::Seedance10Lite,
|
||||
Self::Seedance1p5Pro => CommonModelType::Seedance1p5Pro,
|
||||
Self::Seedance2p0 => CommonModelType::Seedance2p0,
|
||||
@@ -188,6 +192,7 @@ mod tests {
|
||||
assert_serialization(CommonVideoModel::Kling3p0Standard, "kling_3p0_standard");
|
||||
assert_serialization(CommonVideoModel::Kling3p0Pro, "kling_3p0_pro");
|
||||
assert_serialization(CommonVideoModel::HappyHorse1p0, "happy_horse_1p0");
|
||||
assert_serialization(CommonVideoModel::MinimaxH3, "minimax_h3");
|
||||
assert_serialization(CommonVideoModel::Seedance10Lite, "seedance_1p0_lite");
|
||||
assert_serialization(CommonVideoModel::Seedance1p5Pro, "seedance_1p5_pro");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0, "seedance_2p0");
|
||||
@@ -230,6 +235,7 @@ mod tests {
|
||||
("kling_3p0_standard", CommonVideoModel::Kling3p0Standard),
|
||||
("kling_3p0_pro", CommonVideoModel::Kling3p0Pro),
|
||||
("happy_horse_1p0", CommonVideoModel::HappyHorse1p0),
|
||||
("minimax_h3", CommonVideoModel::MinimaxH3),
|
||||
("seedance_1p0_lite", CommonVideoModel::Seedance10Lite),
|
||||
("seedance_1p5_pro", CommonVideoModel::Seedance1p5Pro),
|
||||
("seedance_2p0", CommonVideoModel::Seedance2p0),
|
||||
@@ -279,6 +285,7 @@ mod tests {
|
||||
CommonVideoModel::Kling3p0Standard,
|
||||
CommonVideoModel::Kling3p0Pro,
|
||||
CommonVideoModel::HappyHorse1p0,
|
||||
CommonVideoModel::MinimaxH3,
|
||||
CommonVideoModel::Seedance10Lite,
|
||||
CommonVideoModel::Seedance1p5Pro,
|
||||
CommonVideoModel::Seedance2p0,
|
||||
@@ -327,6 +334,7 @@ mod tests {
|
||||
(CommonVideoModel::Kling3p0Standard, CommonModelType::Kling3p0Standard),
|
||||
(CommonVideoModel::Kling3p0Pro, CommonModelType::Kling3p0Pro),
|
||||
(CommonVideoModel::HappyHorse1p0, CommonModelType::HappyHorse1p0),
|
||||
(CommonVideoModel::MinimaxH3, CommonModelType::MinimaxH3),
|
||||
(CommonVideoModel::Seedance10Lite, CommonModelType::Seedance10Lite),
|
||||
(CommonVideoModel::Seedance1p5Pro, CommonModelType::Seedance1p5Pro),
|
||||
(CommonVideoModel::Seedance2p0, CommonModelType::Seedance2p0),
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
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;
|
||||
|
||||
/// MiniMax (Hailuo) video models.
|
||||
pub fn minimax_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::MinimaxH3,
|
||||
model_creator: Some(ModelCreator::Hailuo),
|
||||
full_name: Some("MiniMax H3".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
text_prompt_max_length: Some(7000),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
// The model renders 768P or 2K; 720p and below land on 768P, 1080p and
|
||||
// above land on 2K.
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TwoK,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TwoK),
|
||||
duration_seconds_min: Some(5),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod grok_video_models;
|
||||
pub mod happy_horse_video_models;
|
||||
pub mod kling_video_models;
|
||||
pub mod minimax_video_models;
|
||||
pub mod seedance_1x_video_models;
|
||||
pub mod seedance_2p0_video_models;
|
||||
pub mod seedance_2p5_video_models;
|
||||
|
||||
@@ -6,6 +6,7 @@ use once_cell::sync::Lazy;
|
||||
use super::by_type::grok_video_models::grok_video_models;
|
||||
use super::by_type::happy_horse_video_models::happy_horse_video_models;
|
||||
use super::by_type::kling_video_models::{kling_disabled_video_models, kling_video_models};
|
||||
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;
|
||||
@@ -30,6 +31,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
models.extend(happy_horse_video_models());
|
||||
models.extend(kling_video_models());
|
||||
models.extend(kling_disabled_video_models());
|
||||
models.extend(minimax_video_models());
|
||||
models.extend(seedance_1p0_video_models());
|
||||
models.extend(seedance_1p5_video_models());
|
||||
models.extend(seedance_2p0_video_models());
|
||||
@@ -99,6 +101,10 @@ fn build_omni_gen_video_model_providers() -> Vec<OmniGenVideoModelProviderDetail
|
||||
model: CommonVideoModel::ViduQ3Turbo,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::MinimaxH3,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling16Pro,
|
||||
overrides: None,
|
||||
|
||||
+1
@@ -155,6 +155,7 @@ fn map_video_model(model: CommonVideoModel) -> Result<RouterVideoModel, HandlerE
|
||||
CommonVideoModel::Seedance2p0BytePlusUltraMini => RouterVideoModel::Seedance2p0BytePlusUltraMini,
|
||||
CommonVideoModel::Seedance2p5Preview => RouterVideoModel::Seedance2p5Preview,
|
||||
CommonVideoModel::HappyHorse1p0 => RouterVideoModel::HappyHorse1p0,
|
||||
CommonVideoModel::MinimaxH3 => RouterVideoModel::MinimaxH3,
|
||||
CommonVideoModel::Sora2 => RouterVideoModel::Sora2,
|
||||
CommonVideoModel::Sora2Pro => RouterVideoModel::Sora2Pro,
|
||||
CommonVideoModel::Veo2 => RouterVideoModel::Veo2,
|
||||
|
||||
+19
@@ -53,6 +53,7 @@ mod tests {
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::test::TestRequest;
|
||||
use actix_web::ResponseError;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
|
||||
use super::*;
|
||||
@@ -74,11 +75,29 @@ mod tests {
|
||||
assert!(response.cost_in_credits.unwrap() > 0);
|
||||
}
|
||||
|
||||
/// MiniMax H3 defaults (5s, 2K) quote at 150 credits; the 768P tier
|
||||
/// (720p and below) quotes at 92 for 5s.
|
||||
#[tokio::test]
|
||||
async fn minimax_h3_quotes_by_resolution_tier() {
|
||||
let default_quote = post_cost_request(base_request(CommonVideoModel::MinimaxH3))
|
||||
.await
|
||||
.expect("minimax h3 default cost estimate should succeed");
|
||||
assert_eq!(default_quote.cost_in_credits, Some(150));
|
||||
|
||||
let mut low_res = base_request(CommonVideoModel::MinimaxH3);
|
||||
low_res.resolution = Some(CommonResolution::SevenTwentyP);
|
||||
let low_res_quote = post_cost_request(low_res)
|
||||
.await
|
||||
.expect("minimax h3 768P-tier cost estimate should succeed");
|
||||
assert_eq!(low_res_quote.cost_in_credits, Some(92));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bare_model_gets_a_quote_for_representative_models() {
|
||||
for model in [
|
||||
CommonVideoModel::GrokImagineVideo,
|
||||
CommonVideoModel::Kling3p0Pro,
|
||||
CommonVideoModel::MinimaxH3,
|
||||
CommonVideoModel::Seedance2p0,
|
||||
CommonVideoModel::Sora2,
|
||||
CommonVideoModel::Veo3p1,
|
||||
|
||||
Reference in New Issue
Block a user