mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-01 23:35:40 -05:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f38d71e8e | ||
|
|
079ccacd51 |
Generated
-2
@@ -770,7 +770,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"artcraft_api_defs",
|
||||
"artcraft_client",
|
||||
"enums",
|
||||
"fal_client",
|
||||
"log",
|
||||
"muapi_client",
|
||||
@@ -14793,7 +14792,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"test_data",
|
||||
"tokio",
|
||||
"uuid 1.22.0",
|
||||
"workspace-build-acceleration",
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ body:
|
||||
type: json
|
||||
content: |
|
||||
{
|
||||
"user_token": "user_rtdhen9kz73kx", //
|
||||
"user_token": "user_jg7ng4kdt2x5y", // newuser946
|
||||
"payments_namespace": "artcraft"
|
||||
}
|
||||
order: 4100053395280358
|
||||
|
||||
+1
-2
@@ -16,8 +16,7 @@ pub struct GenerateVeo2ImageToVideoRequest {
|
||||
|
||||
/// Optional text prompt.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// **NB(bt): ASPECT RATIO IS ACTUALLY NOT USED IN VEO 2 IMAGE-TO-VIDEO**
|
||||
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub aspect_ratio: Option<GenerateVeo2AspectRatio>,
|
||||
|
||||
|
||||
-11
@@ -21,9 +21,6 @@ pub struct GenerateVeo3FastImageToVideoRequest {
|
||||
pub resolution: Option<GenerateVeo3FastResolution>,
|
||||
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub aspect_ratio: Option<GenerateVeo3FastAspectRatio>,
|
||||
|
||||
/// Optional: duration of the generated video.
|
||||
pub duration: Option<GenerateVeo3FastDuration>,
|
||||
|
||||
/// Optional: Generate an audio track along with the video
|
||||
@@ -37,14 +34,6 @@ pub enum GenerateVeo3FastResolution {
|
||||
TenEightyP, // 1080p
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3FastAspectRatio {
|
||||
Auto,
|
||||
SixteenByNine,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3FastDuration {
|
||||
|
||||
-4
@@ -42,10 +42,6 @@ pub struct GenerateVeo3ImageToVideoRequest {
|
||||
pub enum GenerateVeo3AspectRatio {
|
||||
WideSixteenNine, // 16:9
|
||||
TallNineSixteen, // 9:16
|
||||
|
||||
/// NB: DO NOT REMOVE. This is an unsupported resolution for Veo 3, but we can't
|
||||
/// impact existing callers by removing it. The handler will convert this to a
|
||||
/// supported resolution or 400 the request instead.
|
||||
Square, // 1:1
|
||||
}
|
||||
|
||||
|
||||
-9
@@ -2,7 +2,6 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_image_model::CommonImageModel;
|
||||
use enums::common::generation::common_quality::CommonQuality;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
@@ -57,9 +56,6 @@ pub struct OmniGenImageProviderModelDetails {
|
||||
pub struct OmniGenImageModelDetails {
|
||||
pub model: CommonImageModel,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model_creator: Option<ModelCreator>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub full_name: Option<String>,
|
||||
|
||||
@@ -124,16 +120,12 @@ pub struct OmniGenImageModelDetails {
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub batch_size_default: Option<u16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_disabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for OmniGenImageModelDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
model_creator: None,
|
||||
full_name: None,
|
||||
text_prompt_supported: None,
|
||||
text_prompt_max_length: None,
|
||||
@@ -153,7 +145,6 @@ impl Default for OmniGenImageModelDetails {
|
||||
batch_size_max: None,
|
||||
batch_size_options: None,
|
||||
batch_size_default: None,
|
||||
is_disabled: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-9
@@ -2,7 +2,6 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_quality::CommonQuality;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
@@ -66,9 +65,6 @@ pub struct OmniGenVideoModelDetails {
|
||||
|
||||
pub model: CommonVideoModel,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model_creator: Option<ModelCreator>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub full_name: Option<String>,
|
||||
|
||||
@@ -167,16 +163,12 @@ pub struct OmniGenVideoModelDetails {
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub batch_size_default: Option<u16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_disabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for OmniGenVideoModelDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: DEFAULT_VIDEO_MODEL,
|
||||
model_creator: None,
|
||||
full_name: None,
|
||||
text_prompt_supported: None,
|
||||
text_prompt_max_length: None,
|
||||
@@ -210,7 +202,6 @@ impl Default for OmniGenVideoModelDetails {
|
||||
batch_size_max: None,
|
||||
batch_size_options: None,
|
||||
batch_size_default: None,
|
||||
is_disabled: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ pub mod analytics;
|
||||
pub mod credits;
|
||||
pub mod generate;
|
||||
pub mod jobs;
|
||||
pub mod omni_gen;
|
||||
pub mod media_files;
|
||||
pub mod prompts;
|
||||
pub mod stripe_artcraft;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod video;
|
||||
-1
@@ -1 +0,0 @@
|
||||
pub mod omni_gen_video;
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::generate_response::omni_gen_video_generate_response::OmniGenVideoGenerateResponse;
|
||||
|
||||
pub const OMNI_GEN_VIDEO_GENERATE_PATH: &str = "/v1/omni_gen/generate/video";
|
||||
|
||||
pub async fn omni_gen_video_generate(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: OmniGenVideoCostAndGenerateRequest,
|
||||
) -> Result<OmniGenVideoGenerateResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
OMNI_GEN_VIDEO_GENERATE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod generate;
|
||||
@@ -17,7 +17,6 @@ workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
artcraft_api_defs.workspace = true
|
||||
enums.workspace = true
|
||||
artcraft_client.workspace = true
|
||||
fal_client.workspace = true
|
||||
muapi_client.workspace = true
|
||||
|
||||
@@ -38,9 +38,6 @@ pub enum CommonVideoModel {
|
||||
#[serde(rename = "seedance_2p0")]
|
||||
Seedance2p0,
|
||||
|
||||
#[serde(rename = "seedance_2p0_fast")]
|
||||
Seedance2p0Fast,
|
||||
|
||||
#[serde(rename = "sora_2")]
|
||||
Sora2,
|
||||
|
||||
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0BatchCount;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{BatchCount, GenerateVideoArgs, ModelType, Resolution};
|
||||
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
pub(crate) fn estimate_video_cost_artcraft_seedance2p0_fast(
|
||||
plan: &PlanArtcraftSeedance2p0<'_>,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
let duration_seconds = plan.duration_seconds.unwrap_or(5).clamp(4, 15);
|
||||
|
||||
let batch_count = match plan.batch_count {
|
||||
Seedance2p0BatchCount::One => BatchCount::One,
|
||||
Seedance2p0BatchCount::Two => BatchCount::Two,
|
||||
Seedance2p0BatchCount::Four => BatchCount::Four,
|
||||
};
|
||||
|
||||
let dummy_session = Seedance2ProSession::from_cookies_string(String::new());
|
||||
|
||||
let args = GenerateVideoArgs {
|
||||
session: &dummy_session,
|
||||
model_type: ModelType::Seedance2Fast, // <-- Fast, not Pro
|
||||
prompt: String::new(),
|
||||
resolution: Resolution::Square1x1,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = args.estimate_cost_in_usd_cents();
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
|
||||
fn estimate_usd_cents(duration_seconds: u16, video_batch_count: u16) -> u64 {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0Fast,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
idempotency_token: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
fn estimate_pro_usd_cents(duration_seconds: u16, video_batch_count: u16) -> u64 {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
idempotency_token: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_is_present() {
|
||||
assert!(estimate_usd_cents(5, 1) > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_batch_1() {
|
||||
assert_eq!(estimate_usd_cents(4, 1), 51);
|
||||
assert_eq!(estimate_usd_cents(5, 1), 64);
|
||||
assert_eq!(estimate_usd_cents(10, 1), 127);
|
||||
assert_eq!(estimate_usd_cents(15, 1), 191);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(5, 2), 127);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(5, 4), 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_is_cheaper_than_pro() {
|
||||
for duration in [4, 5, 10, 15] {
|
||||
let fast = estimate_usd_cents(duration, 1);
|
||||
let pro = estimate_pro_usd_cents(duration, 1);
|
||||
assert!(
|
||||
fast < pro,
|
||||
"Fast ({}) should be cheaper than Pro ({}) at {}s",
|
||||
fast, pro, duration,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c4 = estimate_usd_cents(4, 1);
|
||||
let c10 = estimate_usd_cents(10, 1);
|
||||
let c15 = estimate_usd_cents(15, 1);
|
||||
assert!(c4 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_batch() {
|
||||
let b1 = estimate_usd_cents(5, 1);
|
||||
let b2 = estimate_usd_cents(5, 2);
|
||||
let b4 = estimate_usd_cents(5, 4);
|
||||
assert!(b1 < b2);
|
||||
assert!(b2 < b4);
|
||||
}
|
||||
}
|
||||
-1
@@ -8,7 +8,6 @@ pub mod estimate_video_cost_artcraft_kling3p0_standard;
|
||||
pub mod estimate_video_cost_artcraft_seedance_1_0_lite;
|
||||
pub mod estimate_video_cost_artcraft_seedance1p5_pro;
|
||||
pub mod estimate_video_cost_artcraft_seedance2p0;
|
||||
pub mod estimate_video_cost_artcraft_seedance2p0_fast;
|
||||
pub mod estimate_video_cost_artcraft_sora_2;
|
||||
pub mod estimate_video_cost_artcraft_sora_2_pro;
|
||||
pub mod estimate_video_cost_artcraft_veo_2;
|
||||
|
||||
-129
@@ -1,129 +0,0 @@
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{GenerateVideoArgs, ModelType, Resolution};
|
||||
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0_fast::PlanSeedance2proSeedance2p0Fast;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
pub(crate) fn estimate_video_cost_seedance2pro_seedance2p0_fast(
|
||||
plan: &PlanSeedance2proSeedance2p0Fast,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
// A dummy session is sufficient — cost estimation does not make any network calls.
|
||||
let dummy_session = Seedance2ProSession::from_cookies_string(String::new());
|
||||
|
||||
let args = GenerateVideoArgs {
|
||||
session: &dummy_session,
|
||||
model_type: ModelType::Seedance2Fast, // <-- Fast, not Pro
|
||||
prompt: String::new(),
|
||||
resolution: Resolution::Square1x1, // Resolution does not affect cost
|
||||
duration_seconds: plan.duration_seconds,
|
||||
batch_count: plan.batch_count,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
|
||||
let cost_in_credits = args.estimate_credits();
|
||||
let cost_in_usd_cents = args.estimate_cost_in_usd_cents();
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_credits as u64),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
|
||||
fn estimate_usd_cents(duration_seconds: u16, video_batch_count: u16) -> u64 {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0Fast,
|
||||
provider: Provider::Seedance2Pro,
|
||||
prompt: None,
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
idempotency_token: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_is_present() {
|
||||
let cents = estimate_usd_cents(5, 1);
|
||||
assert!(cents > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_batch_1() {
|
||||
// Seedance2Fast: 28 credits/sec, 220 credits/$1 (with rounding)
|
||||
assert_eq!(estimate_usd_cents(4, 1), 51);
|
||||
assert_eq!(estimate_usd_cents(5, 1), 64);
|
||||
assert_eq!(estimate_usd_cents(10, 1), 127);
|
||||
assert_eq!(estimate_usd_cents(15, 1), 191);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(5, 2), 127);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(5, 4), 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_is_cheaper_than_pro() {
|
||||
let fast = estimate_usd_cents(5, 1);
|
||||
|
||||
let pro_request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Seedance2Pro,
|
||||
prompt: None,
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
idempotency_token: None,
|
||||
};
|
||||
let pro = pro_request.build().unwrap().estimate_costs().cost_in_usd_cents.unwrap();
|
||||
|
||||
assert!(fast < pro, "Fast ({}) should be cheaper than Pro ({})", fast, pro);
|
||||
}
|
||||
}
|
||||
-1
@@ -1,2 +1 @@
|
||||
pub mod estimate_video_cost_seedance2pro_seedance2p0;
|
||||
pub mod estimate_video_cost_seedance2pro_seedance2p0_fast;
|
||||
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
ArtcraftVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
|
||||
/// Execute Seedance 2.0 Fast via the Artcraft omni-gen video endpoint.
|
||||
///
|
||||
/// Unlike Seedance 2.0 Pro (which uses the legacy dedicated endpoint),
|
||||
/// Seedance 2.0 Fast routes through the omni-gen unified video endpoint.
|
||||
pub async fn execute_artcraft_seedance2p0_fast(
|
||||
plan: &PlanArtcraftSeedance2p0<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let aspect_ratio = plan.aspect_ratio.map(|ar| match ar {
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0AspectRatio::Landscape16x9 => CommonAspectRatio::WideSixteenByNine,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0AspectRatio::Portrait9x16 => CommonAspectRatio::TallNineBySixteen,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0AspectRatio::Square1x1 => CommonAspectRatio::Square,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0AspectRatio::Standard4x3 => CommonAspectRatio::WideFourByThree,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0AspectRatio::Portrait3x4 => CommonAspectRatio::TallThreeByFour,
|
||||
});
|
||||
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some(plan.idempotency_token.clone()),
|
||||
model: Some(CommonVideoModel::Seedance2p0Fast),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: plan.start_frame.cloned(),
|
||||
end_frame_image_media_token: plan.end_frame.cloned(),
|
||||
reference_image_media_tokens: plan.reference_images.cloned(),
|
||||
reference_video_media_tokens: plan.reference_videos.cloned(),
|
||||
reference_audio_media_tokens: plan.reference_audio.cloned(),
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio,
|
||||
quality: None,
|
||||
duration_seconds: plan.duration_seconds.map(|d| d as u16),
|
||||
video_batch_count: Some(match plan.batch_count {
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0BatchCount::One => 1,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0BatchCount::Two => 2,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0BatchCount::Four => 4,
|
||||
}),
|
||||
generate_audio: None,
|
||||
};
|
||||
|
||||
let response = omni_gen_video_generate(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
let all_tokens = vec![response.inference_job_token.clone()];
|
||||
Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
all_inference_job_tokens: all_tokens,
|
||||
}))
|
||||
}
|
||||
+16
-57
@@ -6,72 +6,31 @@ use crate::generate::generate_video::generate_video_response::{
|
||||
};
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_veo_2::PlanArtcraftVeo2;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::generate::video::generate_veo_2_image_to_video::generate_veo_2_image_to_video;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
|
||||
pub async fn execute_artcraft_veo_2(
|
||||
plan: &PlanArtcraftVeo2<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let inference_job_token = match plan.start_frame {
|
||||
Some(media_file_token) => {
|
||||
// Image-to-video: use the legacy dedicated endpoint.
|
||||
let request = GenerateVeo2ImageToVideoRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
media_file_token: Some(media_file_token.to_owned()),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
aspect_ratio: plan.aspect_ratio.clone(),
|
||||
duration: plan.duration.clone(),
|
||||
};
|
||||
|
||||
let response = generate_veo_2_image_to_video(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
response.inference_job_token
|
||||
}
|
||||
None => {
|
||||
// Text-to-video: use the omni-gen unified endpoint.
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some(plan.idempotency_token.clone()),
|
||||
model: Some(CommonVideoModel::Veo2),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: None,
|
||||
end_frame_image_media_token: None,
|
||||
reference_image_media_tokens: None,
|
||||
reference_video_media_tokens: None,
|
||||
reference_audio_media_tokens: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
};
|
||||
|
||||
let response = omni_gen_video_generate(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
response.inference_job_token
|
||||
}
|
||||
let request = GenerateVeo2ImageToVideoRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
media_file_token: Some(plan.start_frame.to_owned()),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
aspect_ratio: plan.aspect_ratio.clone(),
|
||||
duration: plan.duration.clone(),
|
||||
};
|
||||
|
||||
let all_tokens = vec![inference_job_token.clone()];
|
||||
let response = generate_veo_2_image_to_video(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
let all_tokens = vec![response.inference_job_token.clone()];
|
||||
Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload {
|
||||
inference_job_token,
|
||||
inference_job_token: response.inference_job_token,
|
||||
all_inference_job_tokens: all_tokens,
|
||||
}))
|
||||
}
|
||||
|
||||
+18
-59
@@ -6,74 +6,33 @@ use crate::generate::generate_video::generate_video_response::{
|
||||
};
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_veo_3::PlanArtcraftVeo3;
|
||||
use artcraft_api_defs::generate::video::generate_veo_3_image_to_video::GenerateVeo3ImageToVideoRequest;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::generate::video::generate_veo_3_image_to_video::generate_veo_3_image_to_video;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
|
||||
pub async fn execute_artcraft_veo_3(
|
||||
plan: &PlanArtcraftVeo3<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let inference_job_token = match plan.start_frame {
|
||||
Some(media_file_token) => {
|
||||
// Image-to-video: use the legacy dedicated endpoint.
|
||||
let request = GenerateVeo3ImageToVideoRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
media_file_token: Some(media_file_token.to_owned()),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
resolution: plan.resolution,
|
||||
duration: plan.duration,
|
||||
generate_audio: plan.generate_audio,
|
||||
};
|
||||
|
||||
let response = generate_veo_3_image_to_video(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
response.inference_job_token
|
||||
}
|
||||
None => {
|
||||
// Text-to-video: use the omni-gen unified endpoint.
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some(plan.idempotency_token.clone()),
|
||||
model: Some(CommonVideoModel::Veo3),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: None,
|
||||
end_frame_image_media_token: None,
|
||||
reference_image_media_tokens: None,
|
||||
reference_video_media_tokens: None,
|
||||
reference_audio_media_tokens: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: plan.generate_audio,
|
||||
};
|
||||
|
||||
let response = omni_gen_video_generate(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
response.inference_job_token
|
||||
}
|
||||
let request = GenerateVeo3ImageToVideoRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
media_file_token: Some(plan.start_frame.to_owned()),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
resolution: plan.resolution,
|
||||
duration: plan.duration,
|
||||
generate_audio: plan.generate_audio,
|
||||
};
|
||||
|
||||
let all_tokens = vec![inference_job_token.clone()];
|
||||
let response = generate_veo_3_image_to_video(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
let all_tokens = vec![response.inference_job_token.clone()];
|
||||
Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload {
|
||||
inference_job_token,
|
||||
inference_job_token: response.inference_job_token,
|
||||
all_inference_job_tokens: all_tokens,
|
||||
}))
|
||||
}
|
||||
|
||||
+1
-2
@@ -14,9 +14,8 @@ pub async fn execute_artcraft_veo_3_fast(
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let request = GenerateVeo3FastImageToVideoRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
media_file_token: plan.start_frame.map(|t| t.to_owned()),
|
||||
media_file_token: Some(plan.start_frame.to_owned()),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
resolution: plan.resolution,
|
||||
duration: plan.duration,
|
||||
generate_audio: plan.generate_audio,
|
||||
|
||||
-1
@@ -8,7 +8,6 @@ pub mod generate_video_artcraft_kling3p0_standard;
|
||||
pub mod generate_video_artcraft_seedance_1_0_lite;
|
||||
pub mod generate_video_artcraft_seedance1p5_pro;
|
||||
pub mod generate_video_artcraft_seedance2p0;
|
||||
pub mod generate_video_artcraft_seedance2p0_fast;
|
||||
pub mod generate_video_artcraft_sora_2;
|
||||
pub mod generate_video_artcraft_sora_2_pro;
|
||||
pub mod generate_video_artcraft_veo_2;
|
||||
|
||||
+13
-35
@@ -4,50 +4,28 @@ use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
FalVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_2::{
|
||||
FalVeo2Mode, PlanFalVeo2,
|
||||
};
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_2::PlanFalVeo2;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{
|
||||
enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2AspectRatio,
|
||||
};
|
||||
use fal_client::requests::webhook::video::text::enqueue_veo_2_text_to_video_webhook::{
|
||||
enqueue_veo_2_text_to_video_webhook, Veo2TextToVideoArgs,
|
||||
enqueue_veo_2_image_to_video_webhook, Veo2Args,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_veo_2(
|
||||
plan: &PlanFalVeo2,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let webhook_response = match &plan.mode {
|
||||
FalVeo2Mode::TextToVideo => {
|
||||
let args = Veo2TextToVideoArgs {
|
||||
prompt: plan.prompt.as_str(),
|
||||
negative_prompt: plan.negative_prompt.as_deref(),
|
||||
api_key: &fal_client.api_key,
|
||||
duration: plan.duration,
|
||||
aspect_ratio: plan.aspect_ratio.unwrap_or(Veo2AspectRatio::Auto),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
};
|
||||
enqueue_veo_2_text_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
}
|
||||
FalVeo2Mode::ImageToVideo { image_url } => {
|
||||
// Image-to-video does not support aspect_ratio — the output
|
||||
// inherits the source image's aspect ratio.
|
||||
let args = Veo2Args {
|
||||
image_url: image_url.as_str(),
|
||||
prompt: plan.prompt.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
duration: plan.duration,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
};
|
||||
enqueue_veo_2_image_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
}
|
||||
let args = Veo2Args {
|
||||
image_url: plan.start_frame_url.as_str(),
|
||||
prompt: plan.prompt.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
duration: plan.duration,
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
};
|
||||
|
||||
let webhook_response = enqueue_veo_2_image_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
|
||||
+15
-91
@@ -4,106 +4,30 @@ use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
FalVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3::{
|
||||
FalVeo3Duration, FalVeo3I2vAspectRatio, FalVeo3Mode, FalVeo3Resolution,
|
||||
FalVeo3T2vAspectRatio, PlanFalVeo3,
|
||||
};
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3::PlanFalVeo3;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{
|
||||
enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3I2vAspectRatio, Veo3I2vDuration,
|
||||
Veo3I2vResolution,
|
||||
enqueue_veo_3_image_to_video_webhook, Veo3Args,
|
||||
};
|
||||
use fal_client::requests::webhook::video::text::enqueue_veo_3_text_to_video_webhook::{
|
||||
enqueue_veo_3_text_to_video_webhook, Veo3T2vAspectRatio, Veo3T2vDuration, Veo3T2vResolution,
|
||||
Veo3TextToVideoArgs,
|
||||
};
|
||||
|
||||
fn to_i2v_duration(d: FalVeo3Duration) -> Veo3I2vDuration {
|
||||
match d {
|
||||
FalVeo3Duration::Default => Veo3I2vDuration::Default,
|
||||
FalVeo3Duration::FourSeconds => Veo3I2vDuration::FourSeconds,
|
||||
FalVeo3Duration::SixSeconds => Veo3I2vDuration::SixSeconds,
|
||||
FalVeo3Duration::EightSeconds => Veo3I2vDuration::EightSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2v_duration(d: FalVeo3Duration) -> Veo3T2vDuration {
|
||||
match d {
|
||||
FalVeo3Duration::Default => Veo3T2vDuration::Default,
|
||||
FalVeo3Duration::FourSeconds => Veo3T2vDuration::FourSeconds,
|
||||
FalVeo3Duration::SixSeconds => Veo3T2vDuration::SixSeconds,
|
||||
FalVeo3Duration::EightSeconds => Veo3T2vDuration::EightSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_i2v_resolution(r: FalVeo3Resolution) -> Veo3I2vResolution {
|
||||
match r {
|
||||
FalVeo3Resolution::Default => Veo3I2vResolution::Default,
|
||||
FalVeo3Resolution::SevenTwentyP => Veo3I2vResolution::SevenTwentyP,
|
||||
FalVeo3Resolution::TenEightyP => Veo3I2vResolution::TenEightyP,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2v_resolution(r: FalVeo3Resolution) -> Veo3T2vResolution {
|
||||
match r {
|
||||
FalVeo3Resolution::Default => Veo3T2vResolution::Default,
|
||||
FalVeo3Resolution::SevenTwentyP => Veo3T2vResolution::SevenTwentyP,
|
||||
FalVeo3Resolution::TenEightyP => Veo3T2vResolution::TenEightyP,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2v_aspect_ratio(ar: FalVeo3T2vAspectRatio) -> Veo3T2vAspectRatio {
|
||||
match ar {
|
||||
FalVeo3T2vAspectRatio::Default => Veo3T2vAspectRatio::Default,
|
||||
FalVeo3T2vAspectRatio::WideSixteenNine => Veo3T2vAspectRatio::WideSixteenNine,
|
||||
FalVeo3T2vAspectRatio::TallNineSixteen => Veo3T2vAspectRatio::TallNineSixteen,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_i2v_aspect_ratio(ar: FalVeo3I2vAspectRatio) -> Veo3I2vAspectRatio {
|
||||
match ar {
|
||||
FalVeo3I2vAspectRatio::Auto => Veo3I2vAspectRatio::Auto,
|
||||
FalVeo3I2vAspectRatio::WideSixteenNine => Veo3I2vAspectRatio::WideSixteenNine,
|
||||
FalVeo3I2vAspectRatio::TallNineSixteen => Veo3I2vAspectRatio::TallNineSixteen,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute_fal_veo_3(
|
||||
plan: &PlanFalVeo3,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let webhook_response = match &plan.mode {
|
||||
FalVeo3Mode::TextToVideo => {
|
||||
let args = Veo3TextToVideoArgs {
|
||||
prompt: plan.prompt.as_str(),
|
||||
negative_prompt: plan.negative_prompt.as_deref(),
|
||||
api_key: &fal_client.api_key,
|
||||
duration: to_t2v_duration(plan.duration),
|
||||
aspect_ratio: plan.t2v_aspect_ratio.map(to_t2v_aspect_ratio).unwrap_or(Veo3T2vAspectRatio::Default),
|
||||
resolution: to_t2v_resolution(plan.resolution),
|
||||
generate_audio: plan.generate_audio,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
};
|
||||
enqueue_veo_3_text_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
}
|
||||
FalVeo3Mode::ImageToVideo { image_url } => {
|
||||
let args = Veo3Args {
|
||||
image_url: image_url.as_str(),
|
||||
prompt: plan.prompt.as_str(),
|
||||
duration: to_i2v_duration(plan.duration),
|
||||
aspect_ratio: plan.i2v_aspect_ratio.map(to_i2v_aspect_ratio).unwrap_or(Veo3I2vAspectRatio::Auto),
|
||||
resolution: to_i2v_resolution(plan.resolution),
|
||||
generate_audio: plan.generate_audio,
|
||||
api_key: &fal_client.api_key,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
};
|
||||
enqueue_veo_3_image_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
}
|
||||
let args = Veo3Args {
|
||||
image_url: plan.start_frame_url.as_str(),
|
||||
prompt: plan.prompt.as_str(),
|
||||
duration: plan.duration,
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
resolution: plan.resolution,
|
||||
generate_audio: plan.generate_audio,
|
||||
api_key: &fal_client.api_key,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
};
|
||||
|
||||
let webhook_response = enqueue_veo_3_image_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
|
||||
-1
@@ -16,7 +16,6 @@ pub async fn execute_fal_veo_3_fast(
|
||||
let args = Veo3FastArgs {
|
||||
prompt: plan.prompt.as_str(),
|
||||
image_url: plan.start_frame_url.as_str(),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
duration: plan.duration,
|
||||
api_key: &fal_client.api_key,
|
||||
resolution: plan.resolution,
|
||||
|
||||
+3
-3
@@ -60,7 +60,7 @@ pub async fn execute_seedance2pro_seedance2p0(
|
||||
}
|
||||
|
||||
/// Downloads a file from a source URL and re-uploads it to seedance2pro CDN.
|
||||
pub(crate) async fn upload_to_seedance2pro(
|
||||
async fn upload_to_seedance2pro(
|
||||
session: &seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession,
|
||||
source_url: &str,
|
||||
) -> Result<String, ArtcraftRouterError> {
|
||||
@@ -89,7 +89,7 @@ pub(crate) async fn upload_to_seedance2pro(
|
||||
Ok(upload_response.public_url)
|
||||
}
|
||||
|
||||
pub(crate) async fn upload_optional_url(
|
||||
async fn upload_optional_url(
|
||||
session: &seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession,
|
||||
url: Option<&str>,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
@@ -99,7 +99,7 @@ pub(crate) async fn upload_optional_url(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn upload_optional_url_list(
|
||||
async fn upload_optional_url_list(
|
||||
session: &seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession,
|
||||
urls: Option<&[String]>,
|
||||
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
|
||||
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
GenerateVideoResponse, Seedance2proVideoResponsePayload,
|
||||
};
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0_fast::PlanSeedance2proSeedance2p0Fast;
|
||||
use crate::generate::generate_video::execute::seedance2pro::execute_seedance2pro_seedance2p0::{
|
||||
upload_optional_url, upload_optional_url_list,
|
||||
};
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{
|
||||
generate_video, GenerateVideoArgs, ModelType,
|
||||
};
|
||||
|
||||
/// Execute Seedance 2.0 Fast via the Seedance2Pro/Kinovi provider.
|
||||
///
|
||||
/// Identical to the Seedance 2.0 Pro executor except it uses
|
||||
/// `ModelType::Seedance2Fast` instead of `ModelType::Seedance2Pro`.
|
||||
pub async fn execute_seedance2pro_seedance2p0_fast(
|
||||
plan: &PlanSeedance2proSeedance2p0Fast,
|
||||
seedance2pro_client: &RouterSeedance2ProClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let session = &seedance2pro_client.session;
|
||||
|
||||
// Upload media files to seedance2pro CDN
|
||||
let start_frame_url = upload_optional_url(session, plan.start_frame_url.as_deref()).await?;
|
||||
let end_frame_url = upload_optional_url(session, plan.end_frame_url.as_deref()).await?;
|
||||
let reference_image_urls = upload_optional_url_list(session, plan.reference_image_urls.as_deref()).await?;
|
||||
let reference_video_urls = upload_optional_url_list(session, plan.reference_video_urls.as_deref()).await?;
|
||||
let reference_audio_urls = upload_optional_url_list(session, plan.reference_audio_urls.as_deref()).await?;
|
||||
|
||||
let args = GenerateVideoArgs {
|
||||
session,
|
||||
model_type: ModelType::Seedance2Fast, // <-- Fast, not Pro
|
||||
prompt: plan.prompt.clone().unwrap_or_default(),
|
||||
resolution: plan.resolution,
|
||||
duration_seconds: plan.duration_seconds,
|
||||
batch_count: plan.batch_count,
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
|
||||
let response = generate_video(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
}))
|
||||
}
|
||||
-1
@@ -1,2 +1 @@
|
||||
pub mod execute_seedance2pro_seedance2p0;
|
||||
pub mod execute_seedance2pro_seedance2p0_fast;
|
||||
|
||||
-4
@@ -19,7 +19,6 @@ use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraf
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance_1_0_lite::plan_generate_video_artcraft_seedance_1_0_lite;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance1p5_pro::plan_generate_video_artcraft_seedance1p5_pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::plan_generate_video_artcraft_seedance2p0;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0_fast::plan_generate_video_artcraft_seedance2p0_fast;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_sora_2::plan_generate_video_artcraft_sora_2;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_sora_2_pro::plan_generate_video_artcraft_sora_2_pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_veo_2::plan_generate_video_artcraft_veo_2;
|
||||
@@ -45,7 +44,6 @@ use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1:
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1_fast::plan_generate_video_fal_veo_3p1_fast;
|
||||
use crate::generate::generate_video::plan::muapi::plan_generate_video_muapi_seedance2p0::plan_generate_video_muapi_seedance2p0;
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::plan_generate_video_seedance2pro_seedance2p0;
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0_fast::plan_generate_video_seedance2pro_seedance2p0_fast;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
/// Plan to either (1) generate a video or (2) determine how much it costs to generate that video.
|
||||
@@ -133,7 +131,6 @@ impl<'a> GenerateVideoRequest<'a> {
|
||||
CommonVideoModel::Seedance10Lite => plan_generate_video_artcraft_seedance_1_0_lite(self),
|
||||
CommonVideoModel::Seedance1p5Pro => plan_generate_video_artcraft_seedance1p5_pro(self),
|
||||
CommonVideoModel::Seedance2p0 => plan_generate_video_artcraft_seedance2p0(self),
|
||||
CommonVideoModel::Seedance2p0Fast => plan_generate_video_artcraft_seedance2p0_fast(self),
|
||||
CommonVideoModel::Sora2 => plan_generate_video_artcraft_sora_2(self),
|
||||
CommonVideoModel::Sora2Pro => plan_generate_video_artcraft_sora_2_pro(self),
|
||||
CommonVideoModel::Veo2 => plan_generate_video_artcraft_veo_2(self),
|
||||
@@ -177,7 +174,6 @@ impl<'a> GenerateVideoRequest<'a> {
|
||||
fn build_seedance2pro(&self) -> Result<VideoGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.model {
|
||||
CommonVideoModel::Seedance2p0 => plan_generate_video_seedance2pro_seedance2p0(self),
|
||||
CommonVideoModel::Seedance2p0Fast => plan_generate_video_seedance2pro_seedance2p0_fast(self),
|
||||
_ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))),
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -8,7 +8,6 @@ pub mod plan_generate_video_artcraft_kling3p0_standard;
|
||||
pub mod plan_generate_video_artcraft_seedance_1_0_lite;
|
||||
pub mod plan_generate_video_artcraft_seedance1p5_pro;
|
||||
pub mod plan_generate_video_artcraft_seedance2p0;
|
||||
pub mod plan_generate_video_artcraft_seedance2p0_fast;
|
||||
pub mod plan_generate_video_artcraft_sora_2;
|
||||
pub mod plan_generate_video_artcraft_sora_2_pro;
|
||||
pub mod plan_generate_video_artcraft_veo_2;
|
||||
|
||||
-339
@@ -1,339 +0,0 @@
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
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::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
|
||||
Seedance2p0AspectRatio, Seedance2p0BatchCount,
|
||||
};
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
pub fn plan_generate_video_artcraft_seedance2p0_fast<'a>(
|
||||
request: &'a GenerateVideoRequest<'a>,
|
||||
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame = resolve_image_ref(request.start_frame)?;
|
||||
let end_frame = resolve_image_ref(request.end_frame)?;
|
||||
let reference_images = resolve_image_list_ref(request.reference_images)?;
|
||||
let reference_videos = resolve_video_list_ref(request.reference_videos)?;
|
||||
let reference_audio = resolve_audio_list_ref(request.reference_audio)?;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
|
||||
let duration_seconds = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(VideoGenerationPlan::ArtcraftSeedance2p0Fast(PlanArtcraftSeedance2p0 {
|
||||
prompt: request.prompt,
|
||||
start_frame,
|
||||
end_frame,
|
||||
reference_images,
|
||||
reference_videos,
|
||||
reference_audio,
|
||||
reference_characters: resolve_character_list_ref(request.reference_character_tokens),
|
||||
aspect_ratio,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_character_list_ref<'a>(
|
||||
character_list_ref: Option<crate::api::character_list_ref::CharacterListRef<'a>>,
|
||||
) -> Option<&'a Vec<CharacterToken>> {
|
||||
match character_list_ref {
|
||||
None => None,
|
||||
Some(crate::api::character_list_ref::CharacterListRef::CharacterTokens(tokens)) => Some(tokens),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_ref<'a>(
|
||||
image_ref: Option<ImageRef<'a>>,
|
||||
) -> Result<Option<&'a MediaFileToken>, ArtcraftRouterError> {
|
||||
match image_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageRef::MediaFileToken(t)) => Ok(Some(t)),
|
||||
Some(ImageRef::Url(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_video_list_ref<'a>(
|
||||
video_list_ref: Option<VideoListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match video_list_ref {
|
||||
None => Ok(None),
|
||||
Some(VideoListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(VideoListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_audio_list_ref<'a>(
|
||||
audio_list_ref: Option<AudioListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match audio_list_ref {
|
||||
None => Ok(None),
|
||||
Some(AudioListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(AudioListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Seedance2p0AspectRatio>, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(Some(Seedance2p0AspectRatio::Landscape16x9))
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(Some(Seedance2p0AspectRatio::Portrait9x16))
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(Some(Seedance2p0AspectRatio::Square1x1))
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(Seedance2p0AspectRatio::Standard4x3)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(Seedance2p0AspectRatio::Portrait3x4)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(nearest_aspect_ratio(unsupported)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> Seedance2p0AspectRatio {
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => Seedance2p0AspectRatio::Standard4x3,
|
||||
CommonAspectRatio::WideThreeByTwo => Seedance2p0AspectRatio::Standard4x3,
|
||||
CommonAspectRatio::WideTwentyOneByNine => Seedance2p0AspectRatio::Landscape16x9,
|
||||
CommonAspectRatio::TallFourByFive => Seedance2p0AspectRatio::Portrait3x4,
|
||||
CommonAspectRatio::TallTwoByThree => Seedance2p0AspectRatio::Portrait3x4,
|
||||
CommonAspectRatio::TallNineByTwentyOne => Seedance2p0AspectRatio::Portrait9x16,
|
||||
_ => Seedance2p0AspectRatio::Square1x1,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_batch_count(
|
||||
video_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Seedance2p0BatchCount, ArtcraftRouterError> {
|
||||
let count = video_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(Seedance2p0BatchCount::One),
|
||||
2 => Ok(Seedance2p0BatchCount::Two),
|
||||
4 => Ok(Seedance2p0BatchCount::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "video_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
Ok(if count < 4 { Seedance2p0BatchCount::Four } else { Seedance2p0BatchCount::Four })
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(if count < 4 { Seedance2p0BatchCount::Two } else { Seedance2p0BatchCount::Four })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u8>, ArtcraftRouterError> {
|
||||
const MIN: u16 = 4;
|
||||
const MAX: u16 = 15;
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(Some(d as u8)),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(d.clamp(MIN, MAX) as u8)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
|
||||
Seedance2p0AspectRatio, Seedance2p0BatchCount,
|
||||
};
|
||||
|
||||
fn base_request() -> GenerateVideoRequest<'static> {
|
||||
GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0Fast,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn produces_fast_variant() {
|
||||
let req = base_request();
|
||||
let plan = req.build().unwrap();
|
||||
assert!(matches!(plan, VideoGenerationPlan::ArtcraftSeedance2p0Fast(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
let req = base_request();
|
||||
let plan = req.build().unwrap();
|
||||
if let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan {
|
||||
assert_eq!(p.prompt, Some("a cat in space"));
|
||||
} else { panic!("wrong variant"); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_16x9() {
|
||||
let req = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_request()
|
||||
};
|
||||
let plan = req.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert!(matches!(p.aspect_ratio, Some(Seedance2p0AspectRatio::Landscape16x9)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_9x16() {
|
||||
let req = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_request()
|
||||
};
|
||||
let plan = req.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert!(matches!(p.aspect_ratio, Some(Seedance2p0AspectRatio::Portrait9x16)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_square() {
|
||||
let req = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
..base_request()
|
||||
};
|
||||
let plan = req.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert!(matches!(p.aspect_ratio, Some(Seedance2p0AspectRatio::Square1x1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_defaults_to_one() {
|
||||
let req = base_request();
|
||||
let plan = req.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert!(matches!(p.batch_count, Seedance2p0BatchCount::One));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_2() {
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(2), ..base_request() };
|
||||
let plan = req.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert!(matches!(p.batch_count, Seedance2p0BatchCount::Two));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_4() {
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(4), ..base_request() };
|
||||
let plan = req.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert!(matches!(p.batch_count, Seedance2p0BatchCount::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_in_range() {
|
||||
let req = GenerateVideoRequest { duration_seconds: Some(10), ..base_request() };
|
||||
let plan = req.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert_eq!(p.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = GenerateVideoRequest { duration_seconds: Some(99), ..base_request() };
|
||||
let plan = req.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert_eq!(p.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_image_ref_returns_error() {
|
||||
let req = GenerateVideoRequest {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/image.jpg")),
|
||||
..base_request()
|
||||
};
|
||||
assert!(matches!(
|
||||
req.build(),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
}
|
||||
+12
-19
@@ -13,10 +13,7 @@ use tokens::tokens::media_files::MediaFileToken;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftVeo2<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Required for execution. May be None on the cost path when the omni-gen
|
||||
/// distillation hydrated the media token to a URL (which the Artcraft
|
||||
/// provider can't use but cost estimation doesn't need).
|
||||
pub start_frame: Option<&'a MediaFileToken>,
|
||||
pub start_frame: &'a MediaFileToken,
|
||||
pub aspect_ratio: Option<GenerateVeo2AspectRatio>,
|
||||
pub duration: Option<GenerateVeo2Duration>,
|
||||
pub idempotency_token: String,
|
||||
@@ -28,13 +25,16 @@ pub fn plan_generate_video_artcraft_veo_2<'a>(
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame = match request.start_frame {
|
||||
Some(ImageRef::MediaFileToken(t)) => Some(t),
|
||||
// Omni-gen distillation hydrates media tokens to URLs before running the
|
||||
// Artcraft cost path. Cost only depends on duration, so URL-form refs are
|
||||
// accepted and dropped.
|
||||
Some(ImageRef::Url(_)) => None,
|
||||
// No start_frame = text-to-video mode.
|
||||
None => None,
|
||||
Some(ImageRef::MediaFileToken(t)) => t,
|
||||
Some(ImageRef::Url(_)) => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
None => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "start_frame",
|
||||
value: "Veo 2 requires a starting frame".to_string(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
if request.end_frame.is_some() {
|
||||
@@ -44,14 +44,7 @@ pub fn plan_generate_video_artcraft_veo_2<'a>(
|
||||
}));
|
||||
}
|
||||
|
||||
// Aspect ratio only applies to text-to-video; image-to-video inherits
|
||||
// the source frame's aspect ratio.
|
||||
let is_image_mode = request.start_frame.is_some();
|
||||
let aspect_ratio = if is_image_mode {
|
||||
None
|
||||
} else {
|
||||
plan_aspect_ratio(request.aspect_ratio, strategy)?
|
||||
};
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(VideoGenerationPlan::ArtcraftVeo2(PlanArtcraftVeo2 {
|
||||
|
||||
+17
-40
@@ -14,16 +14,10 @@ use tokens::tokens::media_files::MediaFileToken;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftVeo3<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Required for image-to-video. None for text-to-video or when the
|
||||
/// omni-gen distillation hydrated the token to a URL.
|
||||
pub start_frame: Option<&'a MediaFileToken>,
|
||||
pub start_frame: &'a MediaFileToken,
|
||||
pub aspect_ratio: Option<GenerateVeo3AspectRatio>,
|
||||
pub resolution: Option<GenerateVeo3Resolution>,
|
||||
pub duration: Option<GenerateVeo3Duration>,
|
||||
/// The raw requested duration in seconds, preserved for cost estimation.
|
||||
/// The `duration` field above maps everything to `EightSeconds` for the
|
||||
/// legacy endpoint, but cost should reflect the actual requested value.
|
||||
pub duration_seconds_raw: Option<u16>,
|
||||
pub generate_audio: Option<bool>,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
@@ -34,13 +28,16 @@ pub fn plan_generate_video_artcraft_veo_3<'a>(
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame = match request.start_frame {
|
||||
Some(ImageRef::MediaFileToken(t)) => Some(t),
|
||||
// Omni-gen distillation hydrates media tokens to URLs before running the
|
||||
// Artcraft cost path. Cost only depends on duration/audio, so URL-form
|
||||
// refs are accepted and dropped.
|
||||
Some(ImageRef::Url(_)) => None,
|
||||
// No start_frame = text-to-video mode.
|
||||
None => None,
|
||||
Some(ImageRef::MediaFileToken(t)) => t,
|
||||
Some(ImageRef::Url(_)) => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
None => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "start_frame",
|
||||
value: "Veo 3 requires a starting frame".to_string(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
if request.end_frame.is_some() {
|
||||
@@ -50,14 +47,7 @@ pub fn plan_generate_video_artcraft_veo_3<'a>(
|
||||
}));
|
||||
}
|
||||
|
||||
// Aspect ratio only applies to text-to-video; image-to-video inherits
|
||||
// the source frame's aspect ratio.
|
||||
let is_image_mode = request.start_frame.is_some();
|
||||
let aspect_ratio = if is_image_mode {
|
||||
None
|
||||
} else {
|
||||
plan_aspect_ratio(request.aspect_ratio, strategy)?
|
||||
};
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
@@ -67,7 +57,6 @@ pub fn plan_generate_video_artcraft_veo_3<'a>(
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration,
|
||||
duration_seconds_raw: request.duration_seconds,
|
||||
generate_audio: request.generate_audio,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
}))
|
||||
@@ -123,19 +112,14 @@ fn plan_resolution(
|
||||
}
|
||||
}
|
||||
|
||||
// The legacy storyteller endpoint for Veo 3 only exposes EightSeconds, but
|
||||
// the artcraft plan stores the raw duration_seconds from the omni request so
|
||||
// that cost estimation works for all supported durations (4/6/8).
|
||||
// Legacy storyteller endpoint for Veo 3 only exposes EightSeconds.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<GenerateVeo3Duration>, ArtcraftRouterError> {
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
// The api_defs enum only has EightSeconds; all durations map to it for
|
||||
// the legacy endpoint. The actual seconds are tracked via
|
||||
// `duration_seconds_raw` below for cost estimation.
|
||||
Some(4) | Some(6) | Some(8) => Ok(Some(GenerateVeo3Duration::EightSeconds)),
|
||||
Some(8) => Ok(Some(GenerateVeo3Duration::EightSeconds)),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
@@ -143,25 +127,18 @@ fn plan_duration(
|
||||
value: format!("{}", other),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(GenerateVeo3Duration::EightSeconds)),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(GenerateVeo3Duration::EightSeconds)),
|
||||
_ => Ok(Some(GenerateVeo3Duration::EightSeconds)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl PlanArtcraftVeo3<'_> {
|
||||
/// Duration in seconds for cost estimation. Uses the raw requested
|
||||
/// duration, clamped to {4, 6, 8}, defaulting to 8s.
|
||||
/// Mirrors the Fal client default (8s) used by the legacy generate handler.
|
||||
pub fn duration_seconds_for_cost(&self) -> u64 {
|
||||
match self.duration_seconds_raw {
|
||||
Some(s) if s <= 4 => 4,
|
||||
Some(s) if s <= 6 => 6,
|
||||
_ => 8, // None, 7, 8, or above → 8s
|
||||
}
|
||||
8
|
||||
}
|
||||
|
||||
/// Legacy Veo 3 generate handler defaults `generate_audio` to false.
|
||||
/// The omni path defaults to true (matching the fal plan builder).
|
||||
pub fn generate_audio_for_cost(&self) -> bool {
|
||||
self.generate_audio.unwrap_or(false)
|
||||
}
|
||||
|
||||
+12
-36
@@ -1,4 +1,3 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
@@ -7,15 +6,14 @@ use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::generate_veo_3_fast_image_to_video::{
|
||||
GenerateVeo3FastAspectRatio, GenerateVeo3FastDuration, GenerateVeo3FastResolution,
|
||||
GenerateVeo3FastDuration, GenerateVeo3FastResolution,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftVeo3Fast<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub start_frame: Option<&'a MediaFileToken>,
|
||||
pub aspect_ratio: Option<GenerateVeo3FastAspectRatio>,
|
||||
pub start_frame: &'a MediaFileToken,
|
||||
pub resolution: Option<GenerateVeo3FastResolution>,
|
||||
pub duration: Option<GenerateVeo3FastDuration>,
|
||||
pub generate_audio: Option<bool>,
|
||||
@@ -28,9 +26,16 @@ pub fn plan_generate_video_artcraft_veo_3_fast<'a>(
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame = match request.start_frame {
|
||||
Some(ImageRef::MediaFileToken(t)) => Some(t),
|
||||
Some(ImageRef::Url(_)) => None,
|
||||
None => None,
|
||||
Some(ImageRef::MediaFileToken(t)) => t,
|
||||
Some(ImageRef::Url(_)) => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
None => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "start_frame",
|
||||
value: "Veo 3 Fast requires a starting frame".to_string(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
if request.end_frame.is_some() {
|
||||
@@ -40,14 +45,12 @@ pub fn plan_generate_video_artcraft_veo_3_fast<'a>(
|
||||
}));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(VideoGenerationPlan::ArtcraftVeo3Fast(PlanArtcraftVeo3Fast {
|
||||
prompt: request.prompt,
|
||||
start_frame,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration,
|
||||
generate_audio: request.generate_audio,
|
||||
@@ -55,33 +58,6 @@ pub fn plan_generate_video_artcraft_veo_3_fast<'a>(
|
||||
}))
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<GenerateVeo3FastAspectRatio>, ArtcraftRouterError> {
|
||||
use GenerateVeo3FastAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Some(Ar::Auto)),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Ar::SixteenByNine)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Ar::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(Ar::Auto)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
|
||||
+18
-32
@@ -9,22 +9,11 @@ use fal_client::requests::webhook::video::image::enqueue_veo_2_image_to_video_we
|
||||
Veo2AspectRatio, Veo2Duration,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FalVeo2Mode {
|
||||
TextToVideo,
|
||||
ImageToVideo {
|
||||
image_url: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalVeo2 {
|
||||
pub prompt: String,
|
||||
pub negative_prompt: Option<String>,
|
||||
pub mode: FalVeo2Mode,
|
||||
/// Only set for text-to-video. Image-to-video inherits the source frame's
|
||||
/// aspect ratio and doesn't accept this parameter.
|
||||
pub aspect_ratio: Option<Veo2AspectRatio>,
|
||||
pub start_frame_url: String,
|
||||
pub aspect_ratio: Veo2AspectRatio,
|
||||
pub duration: Veo2Duration,
|
||||
}
|
||||
|
||||
@@ -33,37 +22,35 @@ pub fn plan_generate_video_fal_veo_2<'a>(
|
||||
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame_url = resolve_required_image_url(request.start_frame, "start_frame")?;
|
||||
if request.end_frame.is_some() {
|
||||
return Err(unsupported("end_frame", "Veo 2 does not support an ending frame"));
|
||||
}
|
||||
|
||||
let mode = match request.start_frame {
|
||||
Some(ImageRef::Url(url)) => FalVeo2Mode::ImageToVideo {
|
||||
image_url: url.to_string(),
|
||||
},
|
||||
Some(ImageRef::MediaFileToken(_)) => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls));
|
||||
}
|
||||
None => FalVeo2Mode::TextToVideo,
|
||||
};
|
||||
|
||||
// Aspect ratio only applies to text-to-video; image-to-video inherits
|
||||
// the source frame's aspect ratio.
|
||||
let aspect_ratio = match &mode {
|
||||
FalVeo2Mode::TextToVideo => Some(plan_aspect_ratio(request.aspect_ratio, strategy)?),
|
||||
FalVeo2Mode::ImageToVideo { .. } => None,
|
||||
};
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(VideoGenerationPlan::FalVeo2(PlanFalVeo2 {
|
||||
prompt: request.prompt.unwrap_or("").to_string(),
|
||||
negative_prompt: request.negative_prompt.map(|s| s.to_string()),
|
||||
mode,
|
||||
start_frame_url,
|
||||
aspect_ratio,
|
||||
duration,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_required_image_url(
|
||||
image_ref: Option<ImageRef<'_>>,
|
||||
field: &'static str,
|
||||
) -> Result<String, ArtcraftRouterError> {
|
||||
match image_ref {
|
||||
Some(ImageRef::Url(url)) => Ok(url.to_string()),
|
||||
Some(ImageRef::MediaFileToken(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
None => Err(unsupported(field, "Veo 2 requires a starting frame")),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
@@ -73,7 +60,6 @@ fn plan_aspect_ratio(
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Auto),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::WideSixteenNine),
|
||||
|
||||
+38
-117
@@ -7,65 +7,16 @@ use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{
|
||||
Veo3I2vAspectRatio, Veo3I2vDuration, Veo3I2vResolution,
|
||||
Veo3AspectRatio, Veo3Duration, Veo3Resolution,
|
||||
};
|
||||
use fal_client::requests::webhook::video::text::enqueue_veo_3_text_to_video_webhook::{
|
||||
Veo3T2vAspectRatio, Veo3T2vDuration, Veo3T2vResolution,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FalVeo3Mode {
|
||||
TextToVideo,
|
||||
ImageToVideo {
|
||||
image_url: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Router-level duration shared between both modes.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalVeo3Duration {
|
||||
Default,
|
||||
FourSeconds,
|
||||
SixSeconds,
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
/// Router-level resolution shared between both modes.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalVeo3Resolution {
|
||||
Default,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
/// Router-level aspect ratio for text-to-video only (no Auto, no Square).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalVeo3T2vAspectRatio {
|
||||
Default,
|
||||
WideSixteenNine,
|
||||
TallNineSixteen,
|
||||
}
|
||||
|
||||
/// Router-level aspect ratio for image-to-video only (has Auto, no Square).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalVeo3I2vAspectRatio {
|
||||
Auto,
|
||||
WideSixteenNine,
|
||||
TallNineSixteen,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalVeo3 {
|
||||
pub prompt: String,
|
||||
pub negative_prompt: Option<String>,
|
||||
pub mode: FalVeo3Mode,
|
||||
/// Only set for text-to-video. Image-to-video inherits the source frame's
|
||||
/// aspect ratio and uses `i2v_aspect_ratio` instead.
|
||||
pub t2v_aspect_ratio: Option<FalVeo3T2vAspectRatio>,
|
||||
/// Only set for image-to-video.
|
||||
pub i2v_aspect_ratio: Option<FalVeo3I2vAspectRatio>,
|
||||
pub resolution: FalVeo3Resolution,
|
||||
pub duration: FalVeo3Duration,
|
||||
pub start_frame_url: String,
|
||||
pub aspect_ratio: Veo3AspectRatio,
|
||||
pub resolution: Veo3Resolution,
|
||||
pub duration: Veo3Duration,
|
||||
pub generate_audio: bool,
|
||||
}
|
||||
|
||||
@@ -74,88 +25,58 @@ pub fn plan_generate_video_fal_veo_3<'a>(
|
||||
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame_url = resolve_required_image_url(request.start_frame)?;
|
||||
if request.end_frame.is_some() {
|
||||
return Err(unsupported("end_frame", "Veo 3 does not support an ending frame"));
|
||||
}
|
||||
|
||||
let mode = match request.start_frame {
|
||||
Some(ImageRef::Url(url)) => FalVeo3Mode::ImageToVideo {
|
||||
image_url: url.to_string(),
|
||||
},
|
||||
Some(ImageRef::MediaFileToken(_)) => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls));
|
||||
}
|
||||
None => FalVeo3Mode::TextToVideo,
|
||||
};
|
||||
|
||||
let (t2v_aspect_ratio, i2v_aspect_ratio) = match &mode {
|
||||
FalVeo3Mode::TextToVideo => {
|
||||
(Some(plan_t2v_aspect_ratio(request.aspect_ratio, strategy)?), None)
|
||||
}
|
||||
FalVeo3Mode::ImageToVideo { .. } => {
|
||||
(None, Some(plan_i2v_aspect_ratio(request.aspect_ratio, strategy)?))
|
||||
}
|
||||
};
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
let generate_audio = request.generate_audio.unwrap_or(true);
|
||||
|
||||
Ok(VideoGenerationPlan::FalVeo3(PlanFalVeo3 {
|
||||
prompt: request.prompt.unwrap_or("").to_string(),
|
||||
negative_prompt: request.negative_prompt.map(|s| s.to_string()),
|
||||
mode,
|
||||
t2v_aspect_ratio,
|
||||
i2v_aspect_ratio,
|
||||
start_frame_url,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration,
|
||||
generate_audio,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Text-to-video: only 16:9 and 9:16 (no Auto, no Square).
|
||||
fn plan_t2v_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalVeo3T2vAspectRatio, ArtcraftRouterError> {
|
||||
use FalVeo3T2vAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None => Ok(Ar::Default),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::WideSixteenNine),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::TallNineSixteen),
|
||||
|
||||
// Everything else (Auto*, Square, unsupported) → Default (16:9).
|
||||
Some(_) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(unsupported("aspect_ratio", &format!("{:?}", aspect_ratio.unwrap())))
|
||||
}
|
||||
_ => Ok(Ar::Default),
|
||||
},
|
||||
fn resolve_required_image_url(
|
||||
image_ref: Option<ImageRef<'_>>,
|
||||
) -> Result<String, ArtcraftRouterError> {
|
||||
match image_ref {
|
||||
Some(ImageRef::Url(url)) => Ok(url.to_string()),
|
||||
Some(ImageRef::MediaFileToken(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
None => Err(unsupported("start_frame", "Veo 3 requires a starting frame")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Image-to-video: Auto, 16:9, 9:16 (no Square).
|
||||
fn plan_i2v_aspect_ratio(
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalVeo3I2vAspectRatio, ArtcraftRouterError> {
|
||||
use FalVeo3I2vAspectRatio as Ar;
|
||||
) -> Result<Veo3AspectRatio, ArtcraftRouterError> {
|
||||
use Veo3AspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Auto),
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Default),
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::WideSixteenNine),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::TallNineSixteen),
|
||||
|
||||
// Everything else (Square, unsupported) → Auto.
|
||||
Some(_) => match strategy {
|
||||
Some(unsupported_ar) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(unsupported("aspect_ratio", &format!("{:?}", aspect_ratio.unwrap())))
|
||||
Err(unsupported("aspect_ratio", &format!("{:?}", unsupported_ar)))
|
||||
}
|
||||
_ => Ok(Ar::Auto),
|
||||
_ => Ok(Ar::Default),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -163,8 +84,8 @@ fn plan_i2v_aspect_ratio(
|
||||
fn plan_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalVeo3Resolution, ArtcraftRouterError> {
|
||||
use FalVeo3Resolution as R;
|
||||
) -> Result<Veo3Resolution, ArtcraftRouterError> {
|
||||
use Veo3Resolution as R;
|
||||
match resolution {
|
||||
None => Ok(R::Default),
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(R::SevenTwentyP),
|
||||
@@ -182,18 +103,18 @@ fn plan_resolution(
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalVeo3Duration, ArtcraftRouterError> {
|
||||
) -> Result<Veo3Duration, ArtcraftRouterError> {
|
||||
match duration_seconds {
|
||||
None => Ok(FalVeo3Duration::Default),
|
||||
Some(4) => Ok(FalVeo3Duration::FourSeconds),
|
||||
Some(6) => Ok(FalVeo3Duration::SixSeconds),
|
||||
Some(8) => Ok(FalVeo3Duration::EightSeconds),
|
||||
None => Ok(Veo3Duration::Default),
|
||||
Some(4) => Ok(Veo3Duration::FourSeconds),
|
||||
Some(6) => Ok(Veo3Duration::SixSeconds),
|
||||
Some(8) => Ok(Veo3Duration::EightSeconds),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(unsupported("duration_seconds", &format!("{}", other)))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(FalVeo3Duration::EightSeconds),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(FalVeo3Duration::FourSeconds),
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Veo3Duration::EightSeconds),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Veo3Duration::FourSeconds),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -208,9 +129,9 @@ fn unsupported(field: &'static str, value: &str) -> ArtcraftRouterError {
|
||||
impl PlanFalVeo3 {
|
||||
pub fn duration_seconds_for_cost(&self) -> u64 {
|
||||
match self.duration {
|
||||
FalVeo3Duration::Default | FalVeo3Duration::EightSeconds => 8,
|
||||
FalVeo3Duration::SixSeconds => 6,
|
||||
FalVeo3Duration::FourSeconds => 4,
|
||||
Veo3Duration::Default | Veo3Duration::EightSeconds => 8,
|
||||
Veo3Duration::SixSeconds => 6,
|
||||
Veo3Duration::FourSeconds => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-29
@@ -5,16 +5,14 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{
|
||||
Veo3FastAspectRatio, Veo3FastDuration, Veo3FastResolution,
|
||||
Veo3FastDuration, Veo3FastResolution,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalVeo3Fast {
|
||||
pub prompt: String,
|
||||
pub start_frame_url: String,
|
||||
pub aspect_ratio: Veo3FastAspectRatio,
|
||||
pub resolution: Veo3FastResolution,
|
||||
pub duration: Veo3FastDuration,
|
||||
pub generate_audio: bool,
|
||||
@@ -30,7 +28,6 @@ pub fn plan_generate_video_fal_veo_3_fast<'a>(
|
||||
return Err(unsupported("end_frame", "Veo 3 Fast does not support an ending frame"));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
let generate_audio = request.generate_audio.unwrap_or(true);
|
||||
@@ -38,7 +35,6 @@ pub fn plan_generate_video_fal_veo_3_fast<'a>(
|
||||
Ok(VideoGenerationPlan::FalVeo3Fast(PlanFalVeo3Fast {
|
||||
prompt: request.prompt.unwrap_or("").to_string(),
|
||||
start_frame_url,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration,
|
||||
generate_audio,
|
||||
@@ -57,30 +53,6 @@ fn resolve_required_image_url(
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Veo3FastAspectRatio, ArtcraftRouterError> {
|
||||
use Veo3FastAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Auto),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::WideSixteenNine),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::TallNineSixteen),
|
||||
|
||||
Some(unsupported_ar) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(unsupported("aspect_ratio", &format!("{:?}", unsupported_ar)))
|
||||
}
|
||||
_ => Ok(Ar::Auto),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
|
||||
-1
@@ -1,2 +1 @@
|
||||
pub mod plan_generate_video_seedance2pro_seedance2p0;
|
||||
pub mod plan_generate_video_seedance2pro_seedance2p0_fast;
|
||||
|
||||
-198
@@ -1,198 +0,0 @@
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
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::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{BatchCount, Resolution};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanSeedance2proSeedance2p0Fast {
|
||||
pub prompt: Option<String>,
|
||||
pub start_frame_url: Option<String>,
|
||||
pub end_frame_url: Option<String>,
|
||||
pub reference_image_urls: Option<Vec<String>>,
|
||||
pub reference_video_urls: Option<Vec<String>>,
|
||||
pub reference_audio_urls: Option<Vec<String>>,
|
||||
pub resolution: Resolution,
|
||||
pub duration_seconds: u8,
|
||||
pub batch_count: BatchCount,
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_seedance2pro_seedance2p0_fast<'a>(
|
||||
request: &'a GenerateVideoRequest<'a>,
|
||||
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame_url = resolve_image_ref_url(request.start_frame)?;
|
||||
let end_frame_url = resolve_image_ref_url(request.end_frame)?;
|
||||
let reference_image_urls = resolve_image_list_ref_urls(request.reference_images)?;
|
||||
let reference_video_urls = resolve_video_list_ref_urls(request.reference_videos)?;
|
||||
let reference_audio_urls = resolve_audio_list_ref_urls(request.reference_audio)?;
|
||||
|
||||
let resolution = plan_resolution(request.aspect_ratio, strategy)?;
|
||||
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
|
||||
let duration_seconds = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(VideoGenerationPlan::Seedance2proSeedance2p0Fast(PlanSeedance2proSeedance2p0Fast {
|
||||
prompt: request.prompt.map(|p| p.to_string()),
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
resolution,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_ref_url(
|
||||
image_ref: Option<ImageRef<'_>>,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
match image_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageRef::Url(url)) => Ok(Some(url.to_string())),
|
||||
Some(ImageRef::MediaFileToken(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::Seedance2ProOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref_urls(
|
||||
image_list_ref: Option<ImageListRef<'_>>,
|
||||
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(Some(urls.clone())),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::Seedance2ProOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_video_list_ref_urls(
|
||||
video_list_ref: Option<VideoListRef<'_>>,
|
||||
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
|
||||
match video_list_ref {
|
||||
None => Ok(None),
|
||||
Some(VideoListRef::Urls(urls)) => Ok(Some(urls.clone())),
|
||||
Some(VideoListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::Seedance2ProOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_audio_list_ref_urls(
|
||||
audio_list_ref: Option<AudioListRef<'_>>,
|
||||
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
|
||||
match audio_list_ref {
|
||||
None => Ok(None),
|
||||
Some(AudioListRef::Urls(urls)) => Ok(Some(urls.clone())),
|
||||
Some(AudioListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::Seedance2ProOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Resolution, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Resolution::Landscape16x9),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(Resolution::Landscape16x9)
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(Resolution::Portrait9x16)
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(Resolution::Square1x1)
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Resolution::Standard4x3),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Resolution::Portrait3x4),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(nearest_resolution(unsupported))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_resolution(aspect_ratio: CommonAspectRatio) -> Resolution {
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => Resolution::Standard4x3,
|
||||
CommonAspectRatio::WideThreeByTwo => Resolution::Standard4x3,
|
||||
CommonAspectRatio::WideTwentyOneByNine => Resolution::Landscape16x9,
|
||||
CommonAspectRatio::TallFourByFive => Resolution::Portrait3x4,
|
||||
CommonAspectRatio::TallTwoByThree => Resolution::Portrait3x4,
|
||||
CommonAspectRatio::TallNineByTwentyOne => Resolution::Portrait9x16,
|
||||
_ => Resolution::Square1x1,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_batch_count(
|
||||
video_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<BatchCount, ArtcraftRouterError> {
|
||||
let count = video_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(BatchCount::One),
|
||||
2 => Ok(BatchCount::Two),
|
||||
4 => Ok(BatchCount::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "video_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
Ok(if count < 4 { BatchCount::Four } else { BatchCount::Four })
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(if count < 4 { BatchCount::Two } else { BatchCount::Four })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<u8, ArtcraftRouterError> {
|
||||
const MIN: u16 = 4;
|
||||
const MAX: u16 = 15;
|
||||
const DEFAULT: u8 = 5;
|
||||
match duration_seconds {
|
||||
None => Ok(DEFAULT),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(d as u8),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(d.clamp(MIN, MAX) as u8),
|
||||
},
|
||||
}
|
||||
}
|
||||
-21
@@ -10,7 +10,6 @@ use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraf
|
||||
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance_1_0_lite::estimate_video_cost_artcraft_seedance_1_0_lite;
|
||||
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance1p5_pro::estimate_video_cost_artcraft_seedance1p5_pro;
|
||||
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance2p0::estimate_video_cost_artcraft_seedance2p0;
|
||||
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance2p0_fast::estimate_video_cost_artcraft_seedance2p0_fast;
|
||||
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_sora_2::estimate_video_cost_artcraft_sora_2;
|
||||
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_sora_2_pro::estimate_video_cost_artcraft_sora_2_pro;
|
||||
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_veo_2::estimate_video_cost_artcraft_veo_2;
|
||||
@@ -36,7 +35,6 @@ use crate::generate::generate_video::cost::fal::estimate_video_cost_fal_veo_3p1:
|
||||
use crate::generate::generate_video::cost::fal::estimate_video_cost_fal_veo_3p1_fast::estimate_video_cost_fal_veo_3p1_fast;
|
||||
use crate::generate::generate_video::cost::muapi::estimate_video_cost_muapi_seedance2p0::estimate_video_cost_muapi_seedance2p0;
|
||||
use crate::generate::generate_video::cost::seedance2pro::estimate_video_cost_seedance2pro_seedance2p0::estimate_video_cost_seedance2pro_seedance2p0;
|
||||
use crate::generate::generate_video::cost::seedance2pro::estimate_video_cost_seedance2pro_seedance2p0_fast::estimate_video_cost_seedance2pro_seedance2p0_fast;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling_1_6_pro::execute_artcraft_kling_1_6_pro;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling_2_1_master::execute_artcraft_kling_2_1_master;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling_2_1_pro::execute_artcraft_kling_2_1_pro;
|
||||
@@ -47,7 +45,6 @@ use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_seedance_1_0_lite::execute_artcraft_seedance_1_0_lite;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_seedance1p5_pro::execute_artcraft_seedance1p5_pro;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_seedance2p0::execute_artcraft_seedance2p0;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_seedance2p0_fast::execute_artcraft_seedance2p0_fast;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_sora_2::execute_artcraft_sora_2;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_sora_2_pro::execute_artcraft_sora_2_pro;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_veo_2::execute_artcraft_veo_2;
|
||||
@@ -73,7 +70,6 @@ use crate::generate::generate_video::execute::fal::generate_video_fal_veo_3p1::e
|
||||
use crate::generate::generate_video::execute::fal::generate_video_fal_veo_3p1_fast::execute_fal_veo_3p1_fast;
|
||||
use crate::generate::generate_video::execute::muapi::execute_muapi_seedance2p0::execute_muapi_seedance2p0;
|
||||
use crate::generate::generate_video::execute::seedance2pro::execute_seedance2pro_seedance2p0::execute_seedance2pro_seedance2p0;
|
||||
use crate::generate::generate_video::execute::seedance2pro::execute_seedance2pro_seedance2p0_fast::execute_seedance2pro_seedance2p0_fast;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_1_6_pro::PlanArtcraftKling16Pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_2_1_master::PlanArtcraftKling21Master;
|
||||
@@ -110,7 +106,6 @@ use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1:
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1_fast::PlanFalVeo3p1Fast;
|
||||
use crate::generate::generate_video::plan::muapi::plan_generate_video_muapi_seedance2p0::PlanMuapiSeedance2p0;
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::PlanSeedance2proSeedance2p0;
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0_fast::PlanSeedance2proSeedance2p0Fast;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -125,7 +120,6 @@ pub enum VideoGenerationPlan<'a> {
|
||||
ArtcraftSeedance10Lite(PlanArtcraftSeedance10Lite<'a>),
|
||||
ArtcraftSeedance1p5Pro(PlanArtcraftSeedance1p5Pro<'a>),
|
||||
ArtcraftSeedance2p0(PlanArtcraftSeedance2p0<'a>),
|
||||
ArtcraftSeedance2p0Fast(PlanArtcraftSeedance2p0<'a>),
|
||||
ArtcraftSora2(PlanArtcraftSora2<'a>),
|
||||
ArtcraftSora2Pro(PlanArtcraftSora2Pro<'a>),
|
||||
ArtcraftVeo2(PlanArtcraftVeo2<'a>),
|
||||
@@ -135,7 +129,6 @@ pub enum VideoGenerationPlan<'a> {
|
||||
ArtcraftVeo3p1Fast(PlanArtcraftVeo3p1Fast<'a>),
|
||||
MuapiSeedance2p0(PlanMuapiSeedance2p0),
|
||||
Seedance2proSeedance2p0(PlanSeedance2proSeedance2p0),
|
||||
Seedance2proSeedance2p0Fast(PlanSeedance2proSeedance2p0Fast),
|
||||
FalVeo2(PlanFalVeo2),
|
||||
FalVeo3(PlanFalVeo3),
|
||||
FalVeo3Fast(PlanFalVeo3Fast),
|
||||
@@ -200,10 +193,6 @@ impl<'a> VideoGenerationPlan<'a> {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_seedance2p0(plan, artcraft_client).await
|
||||
}
|
||||
VideoGenerationPlan::ArtcraftSeedance2p0Fast(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_seedance2p0_fast(plan, artcraft_client).await
|
||||
}
|
||||
VideoGenerationPlan::ArtcraftSora2(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_sora_2(plan, artcraft_client).await
|
||||
@@ -240,10 +229,6 @@ impl<'a> VideoGenerationPlan<'a> {
|
||||
let seedance2pro_client = client.get_seedance2pro_client_ref()?;
|
||||
execute_seedance2pro_seedance2p0(plan, seedance2pro_client).await
|
||||
}
|
||||
VideoGenerationPlan::Seedance2proSeedance2p0Fast(plan) => {
|
||||
let seedance2pro_client = client.get_seedance2pro_client_ref()?;
|
||||
execute_seedance2pro_seedance2p0_fast(plan, seedance2pro_client).await
|
||||
}
|
||||
VideoGenerationPlan::FalVeo2(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_veo_2(plan, fal_client).await
|
||||
@@ -333,9 +318,6 @@ impl<'a> VideoGenerationPlan<'a> {
|
||||
VideoGenerationPlan::ArtcraftSeedance2p0(plan) => {
|
||||
estimate_video_cost_artcraft_seedance2p0(plan)
|
||||
}
|
||||
VideoGenerationPlan::ArtcraftSeedance2p0Fast(plan) => {
|
||||
estimate_video_cost_artcraft_seedance2p0_fast(plan)
|
||||
}
|
||||
VideoGenerationPlan::ArtcraftSora2(plan) => {
|
||||
estimate_video_cost_artcraft_sora_2(plan)
|
||||
}
|
||||
@@ -353,9 +335,6 @@ impl<'a> VideoGenerationPlan<'a> {
|
||||
VideoGenerationPlan::Seedance2proSeedance2p0(plan) => {
|
||||
estimate_video_cost_seedance2pro_seedance2p0(plan)
|
||||
}
|
||||
VideoGenerationPlan::Seedance2proSeedance2p0Fast(plan) => {
|
||||
estimate_video_cost_seedance2pro_seedance2p0_fast(plan)
|
||||
}
|
||||
VideoGenerationPlan::FalVeo2(plan) => estimate_video_cost_fal_veo_2(plan),
|
||||
VideoGenerationPlan::FalVeo3(plan) => estimate_video_cost_fal_veo_3(plan),
|
||||
VideoGenerationPlan::FalVeo3Fast(plan) => estimate_video_cost_fal_veo_3_fast(plan),
|
||||
|
||||
+4
@@ -7,6 +7,10 @@ pub struct Veo2ImageToVideoInput {
|
||||
|
||||
pub prompt: String,
|
||||
|
||||
/// Options: "auto", "16:9", "9:16"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio: Option<String>,
|
||||
|
||||
/// Options: "4s", "6s", "8s"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration: Option<String>,
|
||||
|
||||
-4
@@ -7,10 +7,6 @@ pub struct Veo3FastImageToVideoInput {
|
||||
|
||||
pub prompt: String,
|
||||
|
||||
/// Options: "auto", "16:9", "9:16"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio: Option<String>,
|
||||
|
||||
/// Options: "720p", "1080p"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution: Option<String>,
|
||||
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
use crate::requests::api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Veo2TextToVideoInput {
|
||||
pub prompt: String,
|
||||
|
||||
/// Options: "16:9", "9:16", "1:1"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio: Option<String>,
|
||||
|
||||
/// Options: "5s", "6s", "7s", "8s"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub negative_prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Veo2TextToVideoVideoFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Veo2TextToVideoOutput {
|
||||
pub video: Veo2TextToVideoVideoFile,
|
||||
}
|
||||
|
||||
pub fn veo_2_text_to_video(
|
||||
params: Veo2TextToVideoInput,
|
||||
) -> FalRequest<Veo2TextToVideoInput, Veo2TextToVideoOutput> {
|
||||
FalRequest::new("fal-ai/veo2", params)
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
use crate::requests::api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Veo3TextToVideoInput {
|
||||
pub prompt: String,
|
||||
|
||||
/// Options: "16:9", "9:16"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio: Option<String>,
|
||||
|
||||
/// Options: "720p", "1080p"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution: Option<String>,
|
||||
|
||||
/// Options: "4s", "6s", "8s"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub generate_audio: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub negative_prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Veo3TextToVideoVideoFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Veo3TextToVideoOutput {
|
||||
pub video: Veo3TextToVideoVideoFile,
|
||||
}
|
||||
|
||||
pub fn veo_3_text_to_video(
|
||||
params: Veo3TextToVideoInput,
|
||||
) -> FalRequest<Veo3TextToVideoInput, Veo3TextToVideoOutput> {
|
||||
FalRequest::new("fal-ai/veo3", params)
|
||||
}
|
||||
@@ -7,7 +7,5 @@ pub mod http_pixverse_v5_text_to_video;
|
||||
pub mod http_seedance_1p5_pro_text_to_video;
|
||||
pub mod http_sora_2_pro_text_to_video;
|
||||
pub mod http_sora_2_text_to_video;
|
||||
pub mod http_veo_2_text_to_video;
|
||||
pub mod http_veo_3_text_to_video;
|
||||
pub mod http_veo_3p1_fast_text_to_video;
|
||||
pub mod http_veo_3p1_text_to_video;
|
||||
|
||||
+19
-12
@@ -1,22 +1,21 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::api::webhook_response::WebhookResponse;
|
||||
use crate::requests::http::video::image::http_veo_2_image_to_video::{veo_2_image_to_video, Veo2ImageToVideoInput};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use crate::requests::http::video::image::http_veo_2_image_to_video::{veo_2_image_to_video, Veo2ImageToVideoInput};
|
||||
use crate::requests::api::webhook_response::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
/// Args for Veo 2 image-to-video. Note: image-to-video does NOT support
|
||||
/// aspect_ratio — the output inherits the source image's aspect ratio.
|
||||
pub struct Veo2Args<'a, U: IntoUrl, V: IntoUrl> {
|
||||
pub image_url: U,
|
||||
pub webhook_url: V,
|
||||
pub prompt: &'a str,
|
||||
pub api_key: &'a FalApiKey,
|
||||
pub duration: Veo2Duration,
|
||||
pub aspect_ratio: Veo2AspectRatio,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo2Duration {
|
||||
Default, // NB: Default is 5 seconds
|
||||
FiveSeconds,
|
||||
@@ -25,9 +24,6 @@ pub enum Veo2Duration {
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
/// Aspect ratio enum — used by the text-to-video endpoint only.
|
||||
/// Kept here because it's the canonical location that the text-to-video
|
||||
/// webhook and the router plan both re-export from.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo2AspectRatio {
|
||||
Auto,
|
||||
@@ -63,13 +59,22 @@ pub async fn enqueue_veo_2_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
Veo2Duration::SevenSeconds => Some("7s".to_string()),
|
||||
Veo2Duration::EightSeconds => Some("8s".to_string()),
|
||||
};
|
||||
|
||||
let aspect_ratio = match args.aspect_ratio {
|
||||
Veo2AspectRatio::Auto => Some("auto".to_string()),
|
||||
Veo2AspectRatio::AutoPreferPortrait => Some("auto_prefer_portrait".to_string()),
|
||||
Veo2AspectRatio::WideSixteenNine => Some("16:9".to_string()),
|
||||
Veo2AspectRatio::TallNineSixteen => Some("9:16".to_string()),
|
||||
};
|
||||
|
||||
let image_url = args.image_url.as_str().to_string();
|
||||
|
||||
let request = Veo2ImageToVideoInput {
|
||||
image_url,
|
||||
prompt: args.prompt.to_string(),
|
||||
aspect_ratio,
|
||||
duration,
|
||||
// Maybe expose these later
|
||||
};
|
||||
|
||||
let result = veo_2_image_to_video(request)
|
||||
@@ -84,24 +89,26 @@ pub async fn enqueue_veo_2_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2Duration};
|
||||
use crate::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2AspectRatio, Veo2Duration};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::MOUNTAIN_TREE_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_veo_2_image_to_video() -> AnyhowResult<()> {
|
||||
async fn test_kling21_pro_video() -> AnyhowResult<()> {
|
||||
let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
|
||||
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Veo2Args {
|
||||
image_url: MOUNTAIN_TREE_IMAGE_URL,
|
||||
image_url: image_url,
|
||||
prompt: "a shot of the mountains as the sun sets and reveals the moon and stars",
|
||||
api_key: &api_key,
|
||||
duration: Veo2Duration::Default,
|
||||
aspect_ratio: Veo2AspectRatio::WideSixteenNine,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
|
||||
+2
-18
@@ -9,7 +9,6 @@ use reqwest::IntoUrl;
|
||||
pub struct Veo3FastArgs<'a, U: IntoUrl, V: IntoUrl> {
|
||||
pub prompt: &'a str,
|
||||
pub image_url: U,
|
||||
pub aspect_ratio: Veo3FastAspectRatio,
|
||||
pub duration: Veo3FastDuration,
|
||||
pub api_key: &'a FalApiKey,
|
||||
pub resolution: Veo3FastResolution,
|
||||
@@ -25,13 +24,6 @@ pub enum Veo3FastDuration {
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3FastAspectRatio {
|
||||
Auto,
|
||||
WideSixteenNine, // 16:9
|
||||
TallNineSixteen, // 9:16
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3FastResolution {
|
||||
Default,
|
||||
@@ -70,13 +62,7 @@ pub async fn enqueue_veo_3_fast_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
Veo3FastDuration::EightSeconds => Some("8s".to_string()),
|
||||
};
|
||||
|
||||
let aspect_ratio = match args.aspect_ratio {
|
||||
Veo3FastAspectRatio::Auto => Some("auto".to_string()),
|
||||
Veo3FastAspectRatio::WideSixteenNine => Some("16:9".to_string()),
|
||||
Veo3FastAspectRatio::TallNineSixteen => Some("9:16".to_string()),
|
||||
};
|
||||
|
||||
let resolution = match args.resolution {
|
||||
let resolution= match args.resolution {
|
||||
Veo3FastResolution::Default => None,
|
||||
Veo3FastResolution::SevenTwentyP => Some("720p".to_string()),
|
||||
Veo3FastResolution::TenEightyP => Some("1080p".to_string()),
|
||||
@@ -87,7 +73,6 @@ pub async fn enqueue_veo_3_fast_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
let request = Veo3FastImageToVideoInput {
|
||||
image_url,
|
||||
prompt: args.prompt.to_string(),
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration,
|
||||
generate_audio: Some(args.generate_audio),
|
||||
@@ -105,7 +90,7 @@ pub async fn enqueue_veo_3_fast_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs, Veo3FastAspectRatio, Veo3FastDuration, Veo3FastResolution};
|
||||
use crate::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs, Veo3FastDuration, Veo3FastResolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::ERNEST_GHOST_TREX_IMAGE_URL;
|
||||
@@ -124,7 +109,6 @@ mod tests {
|
||||
image_url: image_url,
|
||||
prompt: "man is standing next to a ghost and t-rex, they begin to chase him as the camera pulls back to show the wider scene",
|
||||
api_key: &api_key,
|
||||
aspect_ratio: Veo3FastAspectRatio::WideSixteenNine,
|
||||
duration: Veo3FastDuration::EightSeconds,
|
||||
generate_audio: true,
|
||||
resolution: Veo3FastResolution::TenEightyP,
|
||||
|
||||
+39
-41
@@ -9,35 +9,32 @@ use reqwest::IntoUrl;
|
||||
pub struct Veo3Args<'a, U: IntoUrl, V: IntoUrl> {
|
||||
pub image_url: U,
|
||||
pub prompt: &'a str,
|
||||
pub duration: Veo3I2vDuration,
|
||||
pub aspect_ratio: Veo3I2vAspectRatio,
|
||||
pub resolution: Veo3I2vResolution,
|
||||
pub duration: Veo3Duration,
|
||||
pub aspect_ratio: Veo3AspectRatio,
|
||||
pub resolution: Veo3Resolution,
|
||||
pub generate_audio: bool,
|
||||
pub api_key: &'a FalApiKey,
|
||||
pub webhook_url: V,
|
||||
}
|
||||
|
||||
/// Duration for Veo 3 image-to-video. Default is 8 seconds.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3I2vDuration {
|
||||
pub enum Veo3Duration {
|
||||
Default, // Default is 8 seconds
|
||||
FourSeconds,
|
||||
SixSeconds,
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
/// Aspect ratio for Veo 3 image-to-video.
|
||||
/// Supports Auto (inherit source), 16:9, and 9:16. No Square.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3I2vAspectRatio {
|
||||
Auto,
|
||||
WideSixteenNine, // 16:9
|
||||
TallNineSixteen, // 9:16
|
||||
pub enum Veo3AspectRatio {
|
||||
Default,
|
||||
WideSixteenNine,
|
||||
TallNineSixteen,
|
||||
Square,
|
||||
}
|
||||
|
||||
/// Resolution for Veo 3 image-to-video.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3I2vResolution {
|
||||
pub enum Veo3Resolution {
|
||||
Default,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
@@ -51,14 +48,14 @@ impl <U: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Veo3Args<'_, U, V> {
|
||||
// For example, a 5s video with audio on will cost $2."
|
||||
|
||||
match (self.duration, self.generate_audio) {
|
||||
(Veo3I2vDuration::FourSeconds, false) => 80,
|
||||
(Veo3I2vDuration::SixSeconds, false) => 120,
|
||||
(Veo3I2vDuration::EightSeconds, false) => 160,
|
||||
(Veo3I2vDuration::Default, false) => 160,
|
||||
(Veo3I2vDuration::FourSeconds, true) => 160,
|
||||
(Veo3I2vDuration::SixSeconds, true) => 240,
|
||||
(Veo3I2vDuration::EightSeconds, true) => 320,
|
||||
(Veo3I2vDuration::Default, true) => 320,
|
||||
(Veo3Duration::FourSeconds, false) => 80,
|
||||
(Veo3Duration::SixSeconds, false) => 120,
|
||||
(Veo3Duration::EightSeconds, false) => 160,
|
||||
(Veo3Duration::Default, false) => 160, // NB: Default is 8 seconds
|
||||
(Veo3Duration::FourSeconds, true) => 160,
|
||||
(Veo3Duration::SixSeconds, true) => 240,
|
||||
(Veo3Duration::EightSeconds, true) => 320,
|
||||
(Veo3Duration::Default, true) => 320, // NB: Default is 8 seconds320
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,22 +67,23 @@ pub async fn enqueue_veo_3_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
args: Veo3Args<'_, U, V>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
let duration = match args.duration {
|
||||
Veo3I2vDuration::Default => None,
|
||||
Veo3I2vDuration::FourSeconds => Some("4s".to_string()),
|
||||
Veo3I2vDuration::SixSeconds => Some("6s".to_string()),
|
||||
Veo3I2vDuration::EightSeconds => Some("8s".to_string()),
|
||||
Veo3Duration::Default => None,
|
||||
Veo3Duration::FourSeconds => Some("4s".to_string()),
|
||||
Veo3Duration::SixSeconds => Some("6s".to_string()),
|
||||
Veo3Duration::EightSeconds => Some("8s".to_string()),
|
||||
};
|
||||
|
||||
let aspect_ratio = match args.aspect_ratio {
|
||||
Veo3I2vAspectRatio::Auto => Some("auto".to_string()),
|
||||
Veo3I2vAspectRatio::WideSixteenNine => Some("16:9".to_string()),
|
||||
Veo3I2vAspectRatio::TallNineSixteen => Some("9:16".to_string()),
|
||||
Veo3AspectRatio::Default => None,
|
||||
Veo3AspectRatio::WideSixteenNine => Some("16:9".to_string()),
|
||||
Veo3AspectRatio::TallNineSixteen => Some("9:16".to_string()),
|
||||
Veo3AspectRatio::Square => Some("1:1".to_string()),
|
||||
};
|
||||
|
||||
let resolution = match args.resolution {
|
||||
Veo3I2vResolution::Default => None,
|
||||
Veo3I2vResolution::SevenTwentyP => Some("720p".to_string()),
|
||||
Veo3I2vResolution::TenEightyP => Some("1080p".to_string()),
|
||||
|
||||
let resolution= match args.resolution {
|
||||
Veo3Resolution::Default => None,
|
||||
Veo3Resolution::SevenTwentyP => Some("720p".to_string()),
|
||||
Veo3Resolution::TenEightyP => Some("1080p".to_string()),
|
||||
};
|
||||
|
||||
let image_url = args.image_url.as_str().to_string();
|
||||
@@ -111,10 +109,7 @@ pub async fn enqueue_veo_3_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{
|
||||
enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3I2vAspectRatio, Veo3I2vDuration,
|
||||
Veo3I2vResolution,
|
||||
};
|
||||
use crate::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3AspectRatio, Veo3Duration, Veo3Resolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
|
||||
@@ -122,21 +117,24 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Veo3Args {
|
||||
image_url: TREX_SKELETON_IMAGE_URL,
|
||||
prompt: "the t-rex skeleton starts walking towards the camera and roars",
|
||||
api_key: &api_key,
|
||||
duration: Veo3I2vDuration::EightSeconds,
|
||||
aspect_ratio: Veo3I2vAspectRatio::WideSixteenNine,
|
||||
resolution: Veo3I2vResolution::TenEightyP,
|
||||
duration: Veo3Duration::EightSeconds,
|
||||
aspect_ratio: Veo3AspectRatio::WideSixteenNine,
|
||||
resolution: Veo3Resolution::TenEightyP,
|
||||
generate_audio: true,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let _result = enqueue_veo_3_image_to_video_webhook(args).await?;
|
||||
let result = enqueue_veo_3_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::api::webhook_response::WebhookResponse;
|
||||
use crate::requests::http::video::text::http_veo_2_text_to_video::{
|
||||
veo_2_text_to_video, Veo2TextToVideoInput,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{
|
||||
FalRequestCostCalculator, UsdCents,
|
||||
};
|
||||
// Re-use the same duration/aspect-ratio enums as image-to-video.
|
||||
pub use crate::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{
|
||||
Veo2AspectRatio, Veo2Duration,
|
||||
};
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct Veo2TextToVideoArgs<'a, V: IntoUrl> {
|
||||
pub prompt: &'a str,
|
||||
pub negative_prompt: Option<&'a str>,
|
||||
pub duration: Veo2Duration,
|
||||
pub aspect_ratio: Veo2AspectRatio,
|
||||
pub webhook_url: V,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
impl<V: IntoUrl> FalRequestCostCalculator for Veo2TextToVideoArgs<'_, V> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Same pricing as image-to-video:
|
||||
// "For 5s video your request will cost $2.50.
|
||||
// For every additional second you will be charged $0.50."
|
||||
match self.duration {
|
||||
Veo2Duration::Default => 250,
|
||||
Veo2Duration::FiveSeconds => 250,
|
||||
Veo2Duration::SixSeconds => 300,
|
||||
Veo2Duration::SevenSeconds => 350,
|
||||
Veo2Duration::EightSeconds => 400,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Veo 2 Text-to-Video
|
||||
/// https://fal.ai/models/fal-ai/veo2
|
||||
pub async fn enqueue_veo_2_text_to_video_webhook<V: IntoUrl>(
|
||||
args: Veo2TextToVideoArgs<'_, V>,
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
let duration = match args.duration {
|
||||
Veo2Duration::Default => None,
|
||||
Veo2Duration::FiveSeconds => Some("5s".to_string()),
|
||||
Veo2Duration::SixSeconds => Some("6s".to_string()),
|
||||
Veo2Duration::SevenSeconds => Some("7s".to_string()),
|
||||
Veo2Duration::EightSeconds => Some("8s".to_string()),
|
||||
};
|
||||
|
||||
let aspect_ratio = match args.aspect_ratio {
|
||||
Veo2AspectRatio::Auto => None, // Let the API default (16:9)
|
||||
Veo2AspectRatio::AutoPreferPortrait => Some("9:16".to_string()),
|
||||
Veo2AspectRatio::WideSixteenNine => Some("16:9".to_string()),
|
||||
Veo2AspectRatio::TallNineSixteen => Some("9:16".to_string()),
|
||||
};
|
||||
|
||||
let request = Veo2TextToVideoInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
aspect_ratio,
|
||||
duration,
|
||||
negative_prompt: args.negative_prompt.map(|s| s.to_string()),
|
||||
};
|
||||
|
||||
let result = veo_2_text_to_video(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::video::text::enqueue_veo_2_text_to_video_webhook::{
|
||||
enqueue_veo_2_text_to_video_webhook, Veo2TextToVideoArgs, Veo2AspectRatio, Veo2Duration,
|
||||
};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_veo_2_text_to_video() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Veo2TextToVideoArgs {
|
||||
prompt: "a drone shot of a coastal sunset with waves crashing on rocks",
|
||||
negative_prompt: None,
|
||||
api_key: &api_key,
|
||||
duration: Veo2Duration::FiveSeconds,
|
||||
aspect_ratio: Veo2AspectRatio::WideSixteenNine,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let _result = enqueue_veo_2_text_to_video_webhook(args).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::api::webhook_response::WebhookResponse;
|
||||
use crate::requests::http::video::text::http_veo_3_text_to_video::{
|
||||
veo_3_text_to_video, Veo3TextToVideoInput,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{
|
||||
FalRequestCostCalculator, UsdCents,
|
||||
};
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct Veo3TextToVideoArgs<'a, V: IntoUrl> {
|
||||
pub prompt: &'a str,
|
||||
pub negative_prompt: Option<&'a str>,
|
||||
pub duration: Veo3T2vDuration,
|
||||
pub aspect_ratio: Veo3T2vAspectRatio,
|
||||
pub resolution: Veo3T2vResolution,
|
||||
pub generate_audio: bool,
|
||||
pub webhook_url: V,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
/// Duration for Veo 3 text-to-video. Default is 8 seconds.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3T2vDuration {
|
||||
Default, // Default is 8 seconds
|
||||
FourSeconds,
|
||||
SixSeconds,
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
/// Aspect ratio for Veo 3 text-to-video.
|
||||
/// Only 16:9 and 9:16 are supported. No Auto, no Square.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3T2vAspectRatio {
|
||||
Default, // Default is 16:9
|
||||
WideSixteenNine, // 16:9
|
||||
TallNineSixteen, // 9:16
|
||||
}
|
||||
|
||||
/// Resolution for Veo 3 text-to-video.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3T2vResolution {
|
||||
Default,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
impl<V: IntoUrl> FalRequestCostCalculator for Veo3TextToVideoArgs<'_, V> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// $0.20/sec audio off, $0.40/sec audio on.
|
||||
match (self.duration, self.generate_audio) {
|
||||
(Veo3T2vDuration::FourSeconds, false) => 80,
|
||||
(Veo3T2vDuration::SixSeconds, false) => 120,
|
||||
(Veo3T2vDuration::EightSeconds, false) => 160,
|
||||
(Veo3T2vDuration::Default, false) => 160,
|
||||
(Veo3T2vDuration::FourSeconds, true) => 160,
|
||||
(Veo3T2vDuration::SixSeconds, true) => 240,
|
||||
(Veo3T2vDuration::EightSeconds, true) => 320,
|
||||
(Veo3T2vDuration::Default, true) => 320,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Veo 3 Text-to-Video
|
||||
/// https://fal.ai/models/fal-ai/veo3
|
||||
pub async fn enqueue_veo_3_text_to_video_webhook<V: IntoUrl>(
|
||||
args: Veo3TextToVideoArgs<'_, V>,
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
let duration = match args.duration {
|
||||
Veo3T2vDuration::Default => None,
|
||||
Veo3T2vDuration::FourSeconds => Some("4s".to_string()),
|
||||
Veo3T2vDuration::SixSeconds => Some("6s".to_string()),
|
||||
Veo3T2vDuration::EightSeconds => Some("8s".to_string()),
|
||||
};
|
||||
|
||||
let aspect_ratio = match args.aspect_ratio {
|
||||
Veo3T2vAspectRatio::Default => None,
|
||||
Veo3T2vAspectRatio::WideSixteenNine => Some("16:9".to_string()),
|
||||
Veo3T2vAspectRatio::TallNineSixteen => Some("9:16".to_string()),
|
||||
};
|
||||
|
||||
let resolution = match args.resolution {
|
||||
Veo3T2vResolution::Default => None,
|
||||
Veo3T2vResolution::SevenTwentyP => Some("720p".to_string()),
|
||||
Veo3T2vResolution::TenEightyP => Some("1080p".to_string()),
|
||||
};
|
||||
|
||||
let request = Veo3TextToVideoInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration,
|
||||
generate_audio: Some(args.generate_audio),
|
||||
negative_prompt: args.negative_prompt.map(|s| s.to_string()),
|
||||
};
|
||||
|
||||
let result = veo_3_text_to_video(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::video::text::enqueue_veo_3_text_to_video_webhook::{
|
||||
enqueue_veo_3_text_to_video_webhook, Veo3TextToVideoArgs, Veo3T2vAspectRatio,
|
||||
Veo3T2vDuration, Veo3T2vResolution,
|
||||
};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_veo_3_text_to_video() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Veo3TextToVideoArgs {
|
||||
prompt: "a drone shot of a coastal sunset with waves crashing on rocks",
|
||||
negative_prompt: None,
|
||||
api_key: &api_key,
|
||||
duration: Veo3T2vDuration::EightSeconds,
|
||||
aspect_ratio: Veo3T2vAspectRatio::WideSixteenNine,
|
||||
resolution: Veo3T2vResolution::TenEightyP,
|
||||
generate_audio: true,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let _result = enqueue_veo_3_text_to_video_webhook(args).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,5 @@ pub mod enqueue_pixverse_v5_text_to_video_webhook;
|
||||
pub mod enqueue_seedance_1p5_pro_text_to_video_webhook;
|
||||
pub mod enqueue_sora_2_pro_text_to_video_webhook;
|
||||
pub mod enqueue_sora_2_text_to_video_webhook;
|
||||
pub mod enqueue_veo_2_text_to_video_webhook;
|
||||
pub mod enqueue_veo_3_text_to_video_webhook;
|
||||
pub mod enqueue_veo_3p1_fast_text_to_video_webhook;
|
||||
pub mod enqueue_veo_3p1_text_to_video_webhook;
|
||||
@@ -277,8 +277,6 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
|
||||
|
||||
info!("Seedance2pro request : {:?}", request_body);
|
||||
|
||||
println!("\n\nSeedance2pro request : \n\n {:?}\n\n", request_body);
|
||||
|
||||
let cookie = args.session.cookies.as_str();
|
||||
|
||||
let client = Client::builder()
|
||||
|
||||
@@ -34,4 +34,3 @@ env_logger.workspace = true
|
||||
env.workspace = true
|
||||
chrono.workspace = true
|
||||
strum.workspace = true
|
||||
test_data.workspace = true
|
||||
|
||||
@@ -4,26 +4,12 @@ pub enum WorldLabsModel {
|
||||
/// `Marble 0.1-mini` is good for quick drafts
|
||||
/// Generation time: 30-45 seconds
|
||||
/// Cost: 150-330 credits
|
||||
#[deprecated(note="Marble 0.1-mini is deprecated. Use `marble-1.0-draft` instead.")]
|
||||
Marble0p1Mini,
|
||||
|
||||
/// `Marble 0.1-plus` is best for final renders
|
||||
/// Generation time: ~5 minutes,
|
||||
/// Cost: 1500-1600 credits
|
||||
#[deprecated(note="Marble 0.1-plus is deprecated. Use `marble-1.0` instead.")]
|
||||
Marble0p1Plus,
|
||||
|
||||
/// marble-1.0
|
||||
Marble1p0,
|
||||
|
||||
/// marble-1.0-draft
|
||||
Marble1p0Draft,
|
||||
|
||||
/// marble-1.1
|
||||
Marble1p1,
|
||||
|
||||
/// marble-1.1-plus
|
||||
Marble1p1Plus,
|
||||
}
|
||||
|
||||
impl WorldLabsModel {
|
||||
@@ -32,159 +18,6 @@ impl WorldLabsModel {
|
||||
match self {
|
||||
Self::Marble0p1Mini => "Marble 0.1-mini",
|
||||
Self::Marble0p1Plus => "Marble 0.1-plus",
|
||||
Self::Marble1p0 => "marble-1.0",
|
||||
Self::Marble1p0Draft => "marble-1.0-draft",
|
||||
Self::Marble1p1 => "marble-1.1",
|
||||
Self::Marble1p1Plus => "marble-1.1-plus",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_deprecated(&self) -> bool {
|
||||
match self {
|
||||
Self::Marble0p1Mini | Self::Marble0p1Plus => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_new_value(self) -> Self {
|
||||
match self {
|
||||
Self::Marble0p1Mini => Self::Marble1p0Draft,
|
||||
Self::Marble0p1Plus => Self::Marble1p0,
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod api_name {
|
||||
use super::*;
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[test]
|
||||
fn marble_0p1_mini() {
|
||||
assert_eq!(WorldLabsModel::Marble0p1Mini.get_model_api_name_str(), "Marble 0.1-mini");
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[test]
|
||||
fn marble_0p1_plus() {
|
||||
assert_eq!(WorldLabsModel::Marble0p1Plus.get_model_api_name_str(), "Marble 0.1-plus");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marble_1p0() {
|
||||
assert_eq!(WorldLabsModel::Marble1p0.get_model_api_name_str(), "marble-1.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marble_1p0_draft() {
|
||||
assert_eq!(WorldLabsModel::Marble1p0Draft.get_model_api_name_str(), "marble-1.0-draft");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marble_1p1() {
|
||||
assert_eq!(WorldLabsModel::Marble1p1.get_model_api_name_str(), "marble-1.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marble_1p1_plus() {
|
||||
assert_eq!(WorldLabsModel::Marble1p1Plus.get_model_api_name_str(), "marble-1.1-plus");
|
||||
}
|
||||
}
|
||||
|
||||
mod is_deprecated {
|
||||
use super::*;
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[test]
|
||||
fn marble_0p1_mini_is_deprecated() {
|
||||
assert!(WorldLabsModel::Marble0p1Mini.is_deprecated());
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[test]
|
||||
fn marble_0p1_plus_is_deprecated() {
|
||||
assert!(WorldLabsModel::Marble0p1Plus.is_deprecated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marble_1p0_is_not_deprecated() {
|
||||
assert!(!WorldLabsModel::Marble1p0.is_deprecated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marble_1p0_draft_is_not_deprecated() {
|
||||
assert!(!WorldLabsModel::Marble1p0Draft.is_deprecated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marble_1p1_is_not_deprecated() {
|
||||
assert!(!WorldLabsModel::Marble1p1.is_deprecated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marble_1p1_plus_is_not_deprecated() {
|
||||
assert!(!WorldLabsModel::Marble1p1Plus.is_deprecated());
|
||||
}
|
||||
}
|
||||
|
||||
mod to_new_value {
|
||||
use super::*;
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[test]
|
||||
fn marble_0p1_mini_maps_to_1p0_draft() {
|
||||
let new = WorldLabsModel::Marble0p1Mini.to_new_value();
|
||||
assert_eq!(new.get_model_api_name_str(), "marble-1.0-draft");
|
||||
assert!(!new.is_deprecated());
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[test]
|
||||
fn marble_0p1_plus_maps_to_1p0() {
|
||||
let new = WorldLabsModel::Marble0p1Plus.to_new_value();
|
||||
assert_eq!(new.get_model_api_name_str(), "marble-1.0");
|
||||
assert!(!new.is_deprecated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marble_1p0_stays_as_1p0() {
|
||||
let new = WorldLabsModel::Marble1p0.to_new_value();
|
||||
assert_eq!(new.get_model_api_name_str(), "marble-1.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marble_1p0_draft_stays_as_1p0_draft() {
|
||||
let new = WorldLabsModel::Marble1p0Draft.to_new_value();
|
||||
assert_eq!(new.get_model_api_name_str(), "marble-1.0-draft");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marble_1p1_stays_as_1p1() {
|
||||
let new = WorldLabsModel::Marble1p1.to_new_value();
|
||||
assert_eq!(new.get_model_api_name_str(), "marble-1.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marble_1p1_plus_stays_as_1p1_plus() {
|
||||
let new = WorldLabsModel::Marble1p1Plus.to_new_value();
|
||||
assert_eq!(new.get_model_api_name_str(), "marble-1.1-plus");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_models_are_not_deprecated_after_conversion() {
|
||||
for model in [
|
||||
WorldLabsModel::Marble1p0,
|
||||
WorldLabsModel::Marble1p0Draft,
|
||||
WorldLabsModel::Marble1p1,
|
||||
WorldLabsModel::Marble1p1Plus,
|
||||
] {
|
||||
let new = model.to_new_value();
|
||||
assert!(!new.is_deprecated(), "{:?} should not be deprecated after to_new_value", model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-296
@@ -33,15 +33,10 @@ pub struct GenerateWorldResponse {
|
||||
pub async fn generate_world(args: GenerateWorldArgs<'_>) -> Result<GenerateWorldResponse, WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
let model_name = args.model
|
||||
.to_new_value()
|
||||
.get_model_api_name_str()
|
||||
.to_string();
|
||||
|
||||
let payload = RawRequest {
|
||||
world_prompt: args.world_prompt,
|
||||
display_name: args.display_name,
|
||||
model: Some(model_name),
|
||||
model: Some(args.model.get_model_api_name_str().to_string()),
|
||||
seed: args.seed,
|
||||
tags: args.tags,
|
||||
permission: args.permission,
|
||||
@@ -95,11 +90,8 @@ pub async fn generate_world(args: GenerateWorldArgs<'_>) -> Result<GenerateWorld
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::requests::generate_world::http_request::{ContentReference, WorldPrompt};
|
||||
use crate::api::requests::prepare_upload::prepare_upload::{prepare_upload, PrepareUploadArgs, MediaAssetKind};
|
||||
use crate::api::requests::upload_to_signed_url::upload_to_signed_url::{upload_to_signed_url, UploadToSignedUrlArgs};
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use filesys::file_read_bytes::file_read_bytes;
|
||||
use log::LevelFilter;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -159,291 +151,4 @@ mod tests {
|
||||
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
|
||||
/// End-to-end: upload a safe image step by step (prepare → upload →
|
||||
/// generate_world), printing each step's result for debugging.
|
||||
///
|
||||
/// Uses a synthetic gradient PNG from test_data/.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_generate_world_for_safe_image() {
|
||||
setup_test_logging(LevelFilter::Debug);
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
// --- Read file ---
|
||||
let file_bytes = file_read_bytes("/Users/bt/dev/storyteller/artcraft/test_data/image/mochi.jpg")
|
||||
.expect("failed to read safe test image");
|
||||
println!("Read {} bytes from safe test image", file_bytes.len());
|
||||
|
||||
// --- Step 1: prepare_upload ---
|
||||
println!("\n=== Step 1: prepare_upload ===");
|
||||
let prepare_result = prepare_upload(PrepareUploadArgs {
|
||||
creds: &creds,
|
||||
file_name: "mochi.jpg",
|
||||
kind: MediaAssetKind::Image,
|
||||
request_timeout: None,
|
||||
}).await;
|
||||
|
||||
println!("prepare_upload result: {:?}", prepare_result.as_ref().map(|r| &r.media_asset_id));
|
||||
|
||||
let prepare_response = match prepare_result {
|
||||
Ok(r) => {
|
||||
println!(" Media Asset ID: {}", r.media_asset_id.as_str());
|
||||
println!(" Upload URL: {}", r.upload_url);
|
||||
println!(" Required headers: {:?}", r.required_headers);
|
||||
r
|
||||
}
|
||||
Err(err) => {
|
||||
println!(" ERROR at prepare_upload: {:?}", err);
|
||||
panic!("prepare_upload failed: {:?}", err);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Step 2: upload_to_signed_url ---
|
||||
println!("\n=== Step 2: upload_to_signed_url ===");
|
||||
let upload_result = upload_to_signed_url(UploadToSignedUrlArgs {
|
||||
upload_url: &prepare_response.upload_url,
|
||||
file_bytes,
|
||||
required_headers: &prepare_response.required_headers,
|
||||
content_type: "image/jpeg",
|
||||
request_timeout: None,
|
||||
}).await;
|
||||
|
||||
match &upload_result {
|
||||
Ok(()) => println!(" Upload succeeded."),
|
||||
Err(err) => {
|
||||
println!(" ERROR at upload_to_signed_url: {:?}", err);
|
||||
panic!("upload_to_signed_url failed: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 3: generate_world ---
|
||||
println!("\n=== Step 3: generate_world ===");
|
||||
let generate_result = generate_world(GenerateWorldArgs {
|
||||
creds: &creds,
|
||||
world_prompt: WorldPrompt::Image {
|
||||
image_prompt: ContentReference::MediaAsset {
|
||||
media_asset_id: prepare_response.media_asset_id.as_str().to_string(),
|
||||
},
|
||||
text_prompt: Some("A colorful gradient landscape".to_string()),
|
||||
is_pano: None,
|
||||
disable_recaption: None,
|
||||
},
|
||||
display_name: None,
|
||||
model: WorldLabsModel::Marble0p1Mini,
|
||||
seed: None,
|
||||
tags: None,
|
||||
permission: None,
|
||||
request_timeout: None,
|
||||
}).await;
|
||||
|
||||
match &generate_result {
|
||||
Ok(r) => {
|
||||
println!(" SUCCESS — safe image");
|
||||
println!(" Operation ID: {}", r.operation_id.as_str());
|
||||
println!(" Done: {}", r.done);
|
||||
}
|
||||
Err(err) => {
|
||||
println!(" ERROR at generate_world: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
/// End-to-end: upload an NSFW image step by step (prepare → upload →
|
||||
/// generate_world), printing each step's result for debugging.
|
||||
///
|
||||
/// We expect the NSFW rejection to happen at one of these steps.
|
||||
/// This test helps us identify *which* step returns the moderation error
|
||||
/// and what the error response body looks like.
|
||||
///
|
||||
/// NB: Place the NSFW test image at `test_data/images/nsfw_test_image.png`.
|
||||
/// This file is NOT checked into git.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_generate_world_for_nsfw_image() {
|
||||
setup_test_logging(LevelFilter::Debug);
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
// --- Read file ---
|
||||
let file_bytes = file_read_bytes("/Users/bt/Pictures/nsfw_test/nsfw_test_1.jpg")
|
||||
.expect("failed to read NSFW test image");
|
||||
println!("Read {} bytes from NSFW test image", file_bytes.len());
|
||||
|
||||
// --- Step 1: prepare_upload ---
|
||||
println!("\n=== Step 1: prepare_upload ===");
|
||||
let prepare_result = prepare_upload(PrepareUploadArgs {
|
||||
creds: &creds,
|
||||
file_name: "nsfw_test_1.jpg",
|
||||
kind: MediaAssetKind::Image,
|
||||
request_timeout: None,
|
||||
}).await;
|
||||
|
||||
let prepare_response = match prepare_result {
|
||||
Ok(r) => {
|
||||
println!(" OK — Media Asset ID: {}", r.media_asset_id.as_str());
|
||||
println!(" Upload URL: {}", r.upload_url);
|
||||
r
|
||||
}
|
||||
Err(err) => {
|
||||
println!(" NSFW CAUGHT AT STEP 1 (prepare_upload)!");
|
||||
println!(" Error (Debug): {:?}", err);
|
||||
println!(" Error (Display): {}", err);
|
||||
assert_eq!(1, 2, "NSFW caught at prepare_upload — inspect output above");
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
|
||||
// --- Step 2: upload_to_signed_url ---
|
||||
println!("\n=== Step 2: upload_to_signed_url ===");
|
||||
let upload_result = upload_to_signed_url(UploadToSignedUrlArgs {
|
||||
upload_url: &prepare_response.upload_url,
|
||||
file_bytes,
|
||||
required_headers: &prepare_response.required_headers,
|
||||
content_type: "image/jpeg",
|
||||
request_timeout: None,
|
||||
}).await;
|
||||
|
||||
match &upload_result {
|
||||
Ok(()) => println!(" OK — upload succeeded."),
|
||||
Err(err) => {
|
||||
println!(" NSFW CAUGHT AT STEP 2 (upload_to_signed_url)!");
|
||||
println!(" Error (Debug): {:?}", err);
|
||||
println!(" Error (Display): {}", err);
|
||||
assert_eq!(1, 2, "NSFW caught at upload — inspect output above");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 3: generate_world ---
|
||||
println!("\n=== Step 3: generate_world ===");
|
||||
let generate_result = generate_world(GenerateWorldArgs {
|
||||
creds: &creds,
|
||||
world_prompt: WorldPrompt::Image {
|
||||
image_prompt: ContentReference::MediaAsset {
|
||||
media_asset_id: prepare_response.media_asset_id.as_str().to_string(),
|
||||
},
|
||||
text_prompt: None,
|
||||
is_pano: None,
|
||||
disable_recaption: None,
|
||||
},
|
||||
display_name: None,
|
||||
model: WorldLabsModel::Marble0p1Mini,
|
||||
seed: None,
|
||||
tags: None,
|
||||
permission: None,
|
||||
request_timeout: None,
|
||||
}).await;
|
||||
|
||||
match &generate_result {
|
||||
Ok(r) => {
|
||||
println!(" UNEXPECTED SUCCESS — NSFW image was NOT rejected at generate_world!");
|
||||
println!(" Operation ID: {}", r.operation_id.as_str());
|
||||
println!(" Done: {}", r.done);
|
||||
}
|
||||
Err(err) => {
|
||||
println!(" NSFW CAUGHT AT STEP 3 (generate_world)!");
|
||||
println!(" Error (Debug): {:?}", err);
|
||||
println!(" Error (Display): {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
/// Marble 1.1: generate world from a mountain/tree image via URI.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_generate_world_marble_1p1_image_uri() {
|
||||
setup_test_logging(LevelFilter::Debug);
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
let image_url = test_data::web::image_urls::MOUNTAIN_TREE_IMAGE_URL;
|
||||
|
||||
println!("=== generate_world with Marble 1.1 ===");
|
||||
println!("Image URL: {}", image_url);
|
||||
|
||||
let result = generate_world(GenerateWorldArgs {
|
||||
creds: &creds,
|
||||
world_prompt: WorldPrompt::Image {
|
||||
image_prompt: ContentReference::Uri {
|
||||
uri: image_url.to_string(),
|
||||
},
|
||||
text_prompt: Some("Mountain landscape with a tree".to_string()),
|
||||
is_pano: None,
|
||||
disable_recaption: None,
|
||||
},
|
||||
display_name: None,
|
||||
model: WorldLabsModel::Marble1p1,
|
||||
seed: None,
|
||||
tags: None,
|
||||
permission: None,
|
||||
request_timeout: None,
|
||||
}).await;
|
||||
|
||||
match &result {
|
||||
Ok(r) => {
|
||||
println!("SUCCESS — Marble 1.1");
|
||||
println!(" Operation ID: {}", r.operation_id.as_str());
|
||||
println!(" Done: {}", r.done);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("ERROR — Marble 1.1");
|
||||
println!(" Error (Debug): {:?}", err);
|
||||
println!(" Error (Display): {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
/// Marble 1.1-plus: generate world from a wide fall mountains image via URI.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_generate_world_marble_1p1_plus_image_uri() {
|
||||
setup_test_logging(LevelFilter::Debug);
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
let image_url = test_data::web::image_urls::SUPER_WIDE_FALL_MOUNTAINS_IMAGE_URL;
|
||||
|
||||
println!("=== generate_world with Marble 1.1-plus ===");
|
||||
println!("Image URL: {}", image_url);
|
||||
|
||||
let result = generate_world(GenerateWorldArgs {
|
||||
creds: &creds,
|
||||
world_prompt: WorldPrompt::Image {
|
||||
image_prompt: ContentReference::Uri {
|
||||
uri: image_url.to_string(),
|
||||
},
|
||||
text_prompt: Some("Autumn mountain panorama".to_string()),
|
||||
is_pano: None,
|
||||
disable_recaption: None,
|
||||
},
|
||||
display_name: None,
|
||||
model: WorldLabsModel::Marble1p1Plus,
|
||||
seed: None,
|
||||
tags: None,
|
||||
permission: None,
|
||||
request_timeout: None,
|
||||
}).await;
|
||||
|
||||
match &result {
|
||||
Ok(r) => {
|
||||
println!("SUCCESS — Marble 1.1-plus");
|
||||
println!(" Operation ID: {}", r.operation_id.as_str());
|
||||
println!(" Done: {}", r.done);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("ERROR — Marble 1.1-plus");
|
||||
println!(" Error (Debug): {:?}", err);
|
||||
println!(" Error (Display): {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -195,8 +195,7 @@ mod tests {
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
//let operation_id = OperationId("1fab3bf1-05a1-4929-907e-c6df07c539e2".to_string());
|
||||
let operation_id = OperationId("a0894fa6-594d-4680-8f3a-5109f22664b1".to_string());
|
||||
let operation_id = OperationId("1fab3bf1-05a1-4929-907e-c6df07c539e2".to_string());
|
||||
|
||||
let response = get_operation(GetOperationArgs {
|
||||
creds: &creds,
|
||||
|
||||
@@ -16,11 +16,6 @@ pub fn filter_world_labs_http_error(status_code: StatusCode, maybe_body: Option<
|
||||
_ => {},
|
||||
}
|
||||
|
||||
// Check for NSFW content policy rejection (403 with nsfw/sexuality keywords).
|
||||
if let Some(nsfw_error) = check_nsfw_content_rejection(status_code, maybe_body) {
|
||||
return Err(nsfw_error.into());
|
||||
}
|
||||
|
||||
if let Some(body) = maybe_body {
|
||||
return Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code,
|
||||
@@ -33,163 +28,3 @@ pub fn filter_world_labs_http_error(status_code: StatusCode, maybe_body: Option<
|
||||
body: String::new(),
|
||||
}.into())
|
||||
}
|
||||
|
||||
/// Check if a 403 response is an NSFW content policy rejection.
|
||||
/// Returns `Some(WorldLabsSpecificApiError)` if it matches, `None` otherwise.
|
||||
fn check_nsfw_content_rejection(
|
||||
status_code: StatusCode,
|
||||
maybe_body: Option<&str>,
|
||||
) -> Option<WorldLabsSpecificApiError> {
|
||||
if status_code != StatusCode::FORBIDDEN {
|
||||
return None;
|
||||
}
|
||||
|
||||
let body = maybe_body?;
|
||||
let body_lower = body.to_lowercase();
|
||||
|
||||
if body_lower.contains("nsfw") || body_lower.contains("sexuality") {
|
||||
Some(WorldLabsSpecificApiError::NsfwContentPolicyRejected {
|
||||
message: Some(body.to_string()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn success_200_returns_ok() {
|
||||
let result = filter_world_labs_http_error(StatusCode::OK, None);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_200_with_body_returns_ok() {
|
||||
let result = filter_world_labs_http_error(StatusCode::OK, Some("{}"));
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// --- NSFW 403 ---
|
||||
|
||||
#[test]
|
||||
fn nsfw_403_real_response() {
|
||||
let body = r#"{"detail":"NSFW Content is not permitted by policy. Sexuality"}"#;
|
||||
let result = filter_world_labs_http_error(StatusCode::FORBIDDEN, Some(body));
|
||||
let err = result.unwrap_err();
|
||||
|
||||
assert!(err.is_403_forbidden());
|
||||
assert!(matches!(err, WorldLabsError::ApiSpecific(
|
||||
WorldLabsSpecificApiError::NsfwContentPolicyRejected { .. }
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nsfw_403_preserves_message() {
|
||||
let body = r#"{"detail":"NSFW Content is not permitted by policy. Sexuality"}"#;
|
||||
let result = filter_world_labs_http_error(StatusCode::FORBIDDEN, Some(body));
|
||||
let err = result.unwrap_err();
|
||||
|
||||
if let WorldLabsError::ApiSpecific(WorldLabsSpecificApiError::NsfwContentPolicyRejected { message }) = err {
|
||||
assert_eq!(message.as_deref(), Some(body));
|
||||
} else {
|
||||
panic!("expected NsfwContentPolicyRejected, got: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nsfw_403_case_insensitive_nsfw() {
|
||||
let body = r#"{"detail":"nsfw content detected"}"#;
|
||||
let result = filter_world_labs_http_error(StatusCode::FORBIDDEN, Some(body));
|
||||
assert!(matches!(result.unwrap_err(), WorldLabsError::ApiSpecific(
|
||||
WorldLabsSpecificApiError::NsfwContentPolicyRejected { .. }
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nsfw_403_case_insensitive_sexuality() {
|
||||
let body = r#"{"detail":"Rejected due to Sexuality content"}"#;
|
||||
let result = filter_world_labs_http_error(StatusCode::FORBIDDEN, Some(body));
|
||||
assert!(matches!(result.unwrap_err(), WorldLabsError::ApiSpecific(
|
||||
WorldLabsSpecificApiError::NsfwContentPolicyRejected { .. }
|
||||
)));
|
||||
}
|
||||
|
||||
// --- Non-NSFW 403 ---
|
||||
|
||||
#[test]
|
||||
fn non_nsfw_403_is_generic_error() {
|
||||
let body = r#"{"detail":"Forbidden: invalid API key"}"#;
|
||||
let result = filter_world_labs_http_error(StatusCode::FORBIDDEN, Some(body));
|
||||
let err = result.unwrap_err();
|
||||
|
||||
assert!(err.is_403_forbidden());
|
||||
assert!(matches!(err, WorldLabsError::ApiGeneric(
|
||||
WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_nsfw_403_no_body_is_generic_error() {
|
||||
let result = filter_world_labs_http_error(StatusCode::FORBIDDEN, None);
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.is_403_forbidden());
|
||||
assert!(matches!(err, WorldLabsError::ApiGeneric(
|
||||
WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
)));
|
||||
}
|
||||
|
||||
// --- 402 Payment Required ---
|
||||
|
||||
#[test]
|
||||
fn payment_required_402() {
|
||||
let result = filter_world_labs_http_error(StatusCode::PAYMENT_REQUIRED, Some("{}"));
|
||||
let err = result.unwrap_err();
|
||||
assert!(matches!(err, WorldLabsError::ApiSpecific(
|
||||
WorldLabsSpecificApiError::InsufficientCredits
|
||||
)));
|
||||
assert!(!err.is_403_forbidden());
|
||||
}
|
||||
|
||||
// --- Other errors ---
|
||||
|
||||
#[test]
|
||||
fn server_error_500_is_generic() {
|
||||
let body = r#"{"detail":"Internal server error"}"#;
|
||||
let result = filter_world_labs_http_error(StatusCode::INTERNAL_SERVER_ERROR, Some(body));
|
||||
let err = result.unwrap_err();
|
||||
assert!(!err.is_403_forbidden());
|
||||
assert!(matches!(err, WorldLabsError::ApiGeneric(
|
||||
WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_request_400_is_generic() {
|
||||
let body = r#"{"detail":"Bad request"}"#;
|
||||
let result = filter_world_labs_http_error(StatusCode::BAD_REQUEST, Some(body));
|
||||
assert!(matches!(result.unwrap_err(), WorldLabsError::ApiGeneric(
|
||||
WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nsfw_keyword_in_non_403_is_not_nsfw_error() {
|
||||
// Even if the body mentions "nsfw", a 400 should NOT be classified as NSFW rejection.
|
||||
let body = r#"{"detail":"nsfw check failed"}"#;
|
||||
let result = filter_world_labs_http_error(StatusCode::BAD_REQUEST, Some(body));
|
||||
assert!(matches!(result.unwrap_err(), WorldLabsError::ApiGeneric(
|
||||
WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_500_no_body() {
|
||||
let result = filter_world_labs_http_error(StatusCode::INTERNAL_SERVER_ERROR, None);
|
||||
assert!(matches!(result.unwrap_err(), WorldLabsError::ApiGeneric(
|
||||
WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { body, .. }
|
||||
) if body.is_empty()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,6 @@ pub enum WorldLabsError {
|
||||
ApiGeneric(WorldLabsGenericApiError),
|
||||
}
|
||||
|
||||
impl WorldLabsError {
|
||||
pub fn is_403_forbidden(&self) -> bool {
|
||||
match self {
|
||||
Self::ApiSpecific(e) => e.is_403_forbidden(),
|
||||
Self::ApiGeneric(e) => e.is_403_forbidden(),
|
||||
Self::Client(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for WorldLabsError {}
|
||||
|
||||
impl std::fmt::Display for WorldLabsError {
|
||||
|
||||
@@ -24,18 +24,6 @@ pub enum WorldLabsGenericApiError {
|
||||
WreqError(wreq::Error),
|
||||
}
|
||||
|
||||
impl WorldLabsGenericApiError {
|
||||
pub fn is_403_forbidden(&self) -> bool {
|
||||
match self {
|
||||
Self::UncategorizedBadResponseWithStatusAndBody { status_code, .. }
|
||||
| Self::GoogleUploadFailed { status_code, .. } => {
|
||||
*status_code == StatusCode::FORBIDDEN
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for WorldLabsGenericApiError {}
|
||||
|
||||
impl Display for WorldLabsGenericApiError {
|
||||
|
||||
@@ -5,20 +5,6 @@ use std::fmt::{Display, Formatter};
|
||||
pub enum WorldLabsSpecificApiError {
|
||||
/// There aren't sufficient funds for generation.
|
||||
InsufficientCredits,
|
||||
|
||||
/// The content was rejected by WorldLabs' NSFW / content policy filter.
|
||||
NsfwContentPolicyRejected {
|
||||
message: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl WorldLabsSpecificApiError {
|
||||
pub fn is_403_forbidden(&self) -> bool {
|
||||
match self {
|
||||
Self::NsfwContentPolicyRejected { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for WorldLabsSpecificApiError {}
|
||||
@@ -27,13 +13,6 @@ impl Display for WorldLabsSpecificApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InsufficientCredits => write!(f, "Insufficient credits"),
|
||||
Self::NsfwContentPolicyRejected { message } => {
|
||||
write!(f, "NSFW content policy rejection")?;
|
||||
if let Some(msg) = message {
|
||||
write!(f, ": {}", msg)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,38 +42,6 @@ fn credits_for(model: WorldLabsModel, input_type: InputType) -> u32 {
|
||||
(WorldLabsModel::Marble0p1Plus, InputType::ImageNonPanorama) => 1580,
|
||||
(WorldLabsModel::Marble0p1Plus, InputType::MultiImage) => 1600,
|
||||
(WorldLabsModel::Marble0p1Plus, InputType::Video) => 1600,
|
||||
|
||||
// Marble 1.0 (successor to 0.1-plus)
|
||||
(WorldLabsModel::Marble1p0, InputType::ImagePanorama) => 1500,
|
||||
(WorldLabsModel::Marble1p0, InputType::Text) => 1580,
|
||||
(WorldLabsModel::Marble1p0, InputType::ImageNonPanorama) => 1580,
|
||||
(WorldLabsModel::Marble1p0, InputType::MultiImage) => 1600,
|
||||
(WorldLabsModel::Marble1p0, InputType::Video) => 1600,
|
||||
|
||||
// Marble 1.0-draft (successor to 0.1-mini)
|
||||
(WorldLabsModel::Marble1p0Draft, InputType::ImagePanorama) => 150,
|
||||
(WorldLabsModel::Marble1p0Draft, InputType::Text) => 230,
|
||||
(WorldLabsModel::Marble1p0Draft, InputType::ImageNonPanorama) => 230,
|
||||
(WorldLabsModel::Marble1p0Draft, InputType::MultiImage) => 250,
|
||||
(WorldLabsModel::Marble1p0Draft, InputType::Video) => 250,
|
||||
|
||||
// Marble 1.1
|
||||
(WorldLabsModel::Marble1p1, InputType::ImagePanorama) => 1500,
|
||||
(WorldLabsModel::Marble1p1, InputType::Text) => 1580,
|
||||
(WorldLabsModel::Marble1p1, InputType::ImageNonPanorama) => 1580,
|
||||
(WorldLabsModel::Marble1p1, InputType::MultiImage) => 1600,
|
||||
(WorldLabsModel::Marble1p1, InputType::Video) => 1600,
|
||||
|
||||
// TODO: Looks like there's no way for us to know what the price will be
|
||||
// for us to bill this accurately, we'll have to bill this after the fact.
|
||||
// also - they don't have an API for us to ascertain this??! Wat.
|
||||
// Marble 1.1-plus (range pricing — we use the max to charge enough upfront)
|
||||
// Actual range: 1500–3000 / 1580–3080 / 1600–3100
|
||||
(WorldLabsModel::Marble1p1Plus, InputType::ImagePanorama) => 3000,
|
||||
(WorldLabsModel::Marble1p1Plus, InputType::Text) => 3080,
|
||||
(WorldLabsModel::Marble1p1Plus, InputType::ImageNonPanorama) => 3080,
|
||||
(WorldLabsModel::Marble1p1Plus, InputType::MultiImage) => 3100,
|
||||
(WorldLabsModel::Marble1p1Plus, InputType::Video) => 3100,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,10 +64,6 @@ mod tests {
|
||||
let models = [
|
||||
WorldLabsModel::Marble0p1Mini,
|
||||
WorldLabsModel::Marble0p1Plus,
|
||||
WorldLabsModel::Marble1p0,
|
||||
WorldLabsModel::Marble1p0Draft,
|
||||
WorldLabsModel::Marble1p1,
|
||||
WorldLabsModel::Marble1p1Plus,
|
||||
];
|
||||
|
||||
println!("\n{:<20} {:<20} {:>10} {:>12}", "Model", "Input Type", "Credits", "USD Cents");
|
||||
@@ -137,59 +101,6 @@ mod tests {
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::Video).worldlabs_credits, 1600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_marble_1p0_draft_credits() {
|
||||
// Same as Marble 0.1-mini (its successor)
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::Text).worldlabs_credits, 230);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::ImageNonPanorama).worldlabs_credits, 230);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::ImagePanorama).worldlabs_credits, 150);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::MultiImage).worldlabs_credits, 250);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::Video).worldlabs_credits, 250);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_marble_1p0_credits() {
|
||||
// Standard model, same pricing as Marble 0.1-plus
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::Text).worldlabs_credits, 1580);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::ImageNonPanorama).worldlabs_credits, 1580);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::ImagePanorama).worldlabs_credits, 1500);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::MultiImage).worldlabs_credits, 1600);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::Video).worldlabs_credits, 1600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_marble_1p1_credits() {
|
||||
// Standard model, same pricing tier as 1.0
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::Text).worldlabs_credits, 1580);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::ImageNonPanorama).worldlabs_credits, 1580);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::ImagePanorama).worldlabs_credits, 1500);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::MultiImage).worldlabs_credits, 1600);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::Video).worldlabs_credits, 1600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_marble_1p1_plus_credits() {
|
||||
// Plus model uses max of the range pricing (1500–3000, 1580–3080, 1600–3100)
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::Text).worldlabs_credits, 3080);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::ImageNonPanorama).worldlabs_credits, 3080);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::ImagePanorama).worldlabs_credits, 3000);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::MultiImage).worldlabs_credits, 3100);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::Video).worldlabs_credits, 3100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_marble_1p1_plus_is_more_expensive_than_standard() {
|
||||
for input_type in InputType::iter() {
|
||||
let standard = calculate_cost(WorldLabsModel::Marble1p1, input_type);
|
||||
let plus = calculate_cost(WorldLabsModel::Marble1p1Plus, input_type);
|
||||
assert!(
|
||||
plus.worldlabs_credits > standard.worldlabs_credits,
|
||||
"1.1-plus should cost more than 1.1 for {:?}: {} vs {}",
|
||||
input_type, plus.worldlabs_credits, standard.worldlabs_credits,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dollar_conversion() {
|
||||
// 1250 credits = $1.00 = 100 cents
|
||||
@@ -209,10 +120,6 @@ mod tests {
|
||||
let models = [
|
||||
WorldLabsModel::Marble0p1Mini,
|
||||
WorldLabsModel::Marble0p1Plus,
|
||||
WorldLabsModel::Marble1p0,
|
||||
WorldLabsModel::Marble1p0Draft,
|
||||
WorldLabsModel::Marble1p1,
|
||||
WorldLabsModel::Marble1p1Plus,
|
||||
];
|
||||
|
||||
for model in &models {
|
||||
|
||||
-1
@@ -41,7 +41,6 @@ pub (super) async fn handle_artcraft_veo3_fast(
|
||||
generate_audio: request.generate_audio,
|
||||
duration: None, // TODO: Parameterize
|
||||
resolution: None, // TODO: Parameterize
|
||||
aspect_ratio: None, // TODO: Parameterize
|
||||
};
|
||||
|
||||
let result = generate_veo_3_fast_image_to_video(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
/// The version of the ArtCraft desktop app reported in the UI and via Analytics.
|
||||
/// This should match the CI builds.
|
||||
pub const ARTCRAFT_VERSION: &str = "0.28.0";
|
||||
pub const ARTCRAFT_VERSION: &str = "0.27.0";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.28.0",
|
||||
"version": "0.27.0",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/artcraft/dist",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.28.0",
|
||||
"version": "0.27.0",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/artcraft/dist",
|
||||
|
||||
@@ -24,10 +24,6 @@ pub struct NotificationDetails {
|
||||
/// Whether this notification originated from an error.
|
||||
pub(crate) is_from_error: bool,
|
||||
|
||||
/// Extra side-channel messages, eg. if we know we're sending an error/exception,
|
||||
/// but want to include an additional message. (The "description" field is messy atm.)
|
||||
pub(crate) extra_message: Option<String>,
|
||||
|
||||
/// HTTP method associated with the event, if any.
|
||||
pub(crate) http_method: Option<String>,
|
||||
|
||||
@@ -94,30 +90,25 @@ impl NotificationDetails {
|
||||
sections.push(desc.clone());
|
||||
}
|
||||
|
||||
// 2. Extra message
|
||||
if let Some(message) = &self.extra_message {
|
||||
sections.push(message.clone());
|
||||
}
|
||||
|
||||
// 3. Error chain
|
||||
// 2. Error chain
|
||||
if let Some(error_section) = self.format_error_chain() {
|
||||
sections.push(error_section);
|
||||
}
|
||||
|
||||
// 4. Event time
|
||||
// 3. Event time
|
||||
sections.push(self.format_event_time());
|
||||
|
||||
// 5. HTTP context
|
||||
// 4. HTTP context
|
||||
if let Some(http_section) = self.format_http_context() {
|
||||
sections.push(http_section);
|
||||
}
|
||||
|
||||
// 6. Service tokens
|
||||
// 5. Service tokens
|
||||
if let Some(tokens_section) = self.format_service_tokens() {
|
||||
sections.push(tokens_section);
|
||||
}
|
||||
|
||||
// 7. Application/service identity and hostname
|
||||
// 6. Application/service identity and hostname
|
||||
if let Some(identity_section) = Self::format_app_identity(application_name, service_id, hostname) {
|
||||
sections.push(identity_section);
|
||||
}
|
||||
|
||||
@@ -14,10 +14,6 @@ pub struct NotificationDetailsBuilder {
|
||||
pub(crate) maybe_error: Option<Arc<dyn std::error::Error + Send + Sync + 'static>>,
|
||||
pub(crate) is_from_error: bool,
|
||||
|
||||
/// Extra side-channel messages, eg. if we know we're sending an error/exception,
|
||||
/// but want to include an additional message. (The "description" field is messy atm.)
|
||||
pub(crate) extra_message: Option<String>,
|
||||
|
||||
pub(crate) http_method: Option<String>,
|
||||
pub(crate) http_path: Option<String>,
|
||||
pub(crate) http_status_code: Option<u16>,
|
||||
@@ -45,7 +41,6 @@ impl NotificationDetailsBuilder {
|
||||
event_time: Utc::now(),
|
||||
maybe_error: None,
|
||||
is_from_error: false,
|
||||
extra_message: None,
|
||||
http_method: None,
|
||||
http_path: None,
|
||||
http_status_code: None,
|
||||
@@ -75,7 +70,6 @@ impl NotificationDetailsBuilder {
|
||||
event_time: Utc::now(),
|
||||
maybe_error: Some(error),
|
||||
is_from_error: true,
|
||||
extra_message: None,
|
||||
http_method: None,
|
||||
http_path: None,
|
||||
http_status_code: None,
|
||||
@@ -112,11 +106,6 @@ impl NotificationDetailsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_extra_message(mut self, extra_message: Option<String>) -> Self {
|
||||
self.extra_message = extra_message;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_http_method(mut self, http_method: Option<String>) -> Self {
|
||||
self.http_method = http_method;
|
||||
self
|
||||
@@ -187,7 +176,6 @@ impl NotificationDetailsBuilder {
|
||||
event_time: self.event_time,
|
||||
maybe_error: self.maybe_error,
|
||||
is_from_error: self.is_from_error,
|
||||
extra_message: self.extra_message,
|
||||
http_method: self.http_method,
|
||||
http_path: self.http_path,
|
||||
http_status_code: self.http_status_code,
|
||||
|
||||
@@ -28,14 +28,6 @@ pub enum StaffAuditAction {
|
||||
/// Staff unbanned a user.
|
||||
#[serde(rename = "unban_user")]
|
||||
UnbanUser,
|
||||
|
||||
/// Staff added banked balance to a wallet.
|
||||
#[serde(rename = "add_wallet_banked_balance")]
|
||||
AddWalletBankedBalance,
|
||||
|
||||
/// Staff sent a manual alert via the pager system.
|
||||
#[serde(rename = "send_alert")]
|
||||
SendAlert,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(StaffAuditAction);
|
||||
@@ -49,8 +41,6 @@ impl StaffAuditAction {
|
||||
Self::ImpersonateUserRedeem => "impersonate_user_redeem",
|
||||
Self::BanUser => "ban_user",
|
||||
Self::UnbanUser => "unban_user",
|
||||
Self::AddWalletBankedBalance => "add_wallet_banked_balance",
|
||||
Self::SendAlert => "send_alert",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +50,6 @@ impl StaffAuditAction {
|
||||
"impersonate_user_redeem" => Ok(Self::ImpersonateUserRedeem),
|
||||
"ban_user" => Ok(Self::BanUser),
|
||||
"unban_user" => Ok(Self::UnbanUser),
|
||||
"add_wallet_banked_balance" => Ok(Self::AddWalletBankedBalance),
|
||||
"send_alert" => Ok(Self::SendAlert),
|
||||
_ => Err(format!("invalid StaffAuditAction value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
@@ -72,8 +60,6 @@ impl StaffAuditAction {
|
||||
Self::ImpersonateUserRedeem,
|
||||
Self::BanUser,
|
||||
Self::UnbanUser,
|
||||
Self::AddWalletBankedBalance,
|
||||
Self::SendAlert,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -92,8 +78,6 @@ mod tests {
|
||||
assert_serialization(StaffAuditAction::ImpersonateUserRedeem, "impersonate_user_redeem");
|
||||
assert_serialization(StaffAuditAction::BanUser, "ban_user");
|
||||
assert_serialization(StaffAuditAction::UnbanUser, "unban_user");
|
||||
assert_serialization(StaffAuditAction::AddWalletBankedBalance, "add_wallet_banked_balance");
|
||||
assert_serialization(StaffAuditAction::SendAlert, "send_alert");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -102,8 +86,6 @@ mod tests {
|
||||
assert_eq!(StaffAuditAction::ImpersonateUserRedeem.to_str(), "impersonate_user_redeem");
|
||||
assert_eq!(StaffAuditAction::BanUser.to_str(), "ban_user");
|
||||
assert_eq!(StaffAuditAction::UnbanUser.to_str(), "unban_user");
|
||||
assert_eq!(StaffAuditAction::AddWalletBankedBalance.to_str(), "add_wallet_banked_balance");
|
||||
assert_eq!(StaffAuditAction::SendAlert.to_str(), "send_alert");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -112,14 +94,12 @@ mod tests {
|
||||
assert_eq!(StaffAuditAction::from_str("impersonate_user_redeem").unwrap(), StaffAuditAction::ImpersonateUserRedeem);
|
||||
assert_eq!(StaffAuditAction::from_str("ban_user").unwrap(), StaffAuditAction::BanUser);
|
||||
assert_eq!(StaffAuditAction::from_str("unban_user").unwrap(), StaffAuditAction::UnbanUser);
|
||||
assert_eq!(StaffAuditAction::from_str("add_wallet_banked_balance").unwrap(), StaffAuditAction::AddWalletBankedBalance);
|
||||
assert_eq!(StaffAuditAction::from_str("send_alert").unwrap(), StaffAuditAction::SendAlert);
|
||||
assert!(StaffAuditAction::from_str("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
const EXPECTED_COUNT: usize = 6;
|
||||
const EXPECTED_COUNT: usize = 4;
|
||||
assert_eq!(StaffAuditAction::all_variants().len(), EXPECTED_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-11
@@ -16,10 +16,6 @@ pub enum StaffAuditEntityType {
|
||||
/// A user account.
|
||||
#[serde(rename = "user")]
|
||||
User,
|
||||
|
||||
/// A wallet.
|
||||
#[serde(rename = "wallet")]
|
||||
Wallet,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(StaffAuditEntityType);
|
||||
@@ -30,14 +26,12 @@ impl StaffAuditEntityType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::User => "user",
|
||||
Self::Wallet => "wallet",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"user" => Ok(Self::User),
|
||||
"wallet" => Ok(Self::Wallet),
|
||||
_ => Err(format!("invalid StaffAuditEntityType value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
@@ -45,7 +39,6 @@ impl StaffAuditEntityType {
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
BTreeSet::from([
|
||||
Self::User,
|
||||
Self::Wallet,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -61,25 +54,22 @@ mod tests {
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(StaffAuditEntityType::User, "user");
|
||||
assert_serialization(StaffAuditEntityType::Wallet, "wallet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(StaffAuditEntityType::User.to_str(), "user");
|
||||
assert_eq!(StaffAuditEntityType::Wallet.to_str(), "wallet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(StaffAuditEntityType::from_str("user").unwrap(), StaffAuditEntityType::User);
|
||||
assert_eq!(StaffAuditEntityType::from_str("wallet").unwrap(), StaffAuditEntityType::Wallet);
|
||||
assert!(StaffAuditEntityType::from_str("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
const EXPECTED_COUNT: usize = 2;
|
||||
const EXPECTED_COUNT: usize = 1;
|
||||
assert_eq!(StaffAuditEntityType::all_variants().len(), EXPECTED_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +124,6 @@ pub enum CommonModelType {
|
||||
Seedance1p5Pro,
|
||||
#[serde(rename = "seedance_2p0")]
|
||||
Seedance2p0,
|
||||
#[serde(rename = "seedance_2p0_fast")]
|
||||
Seedance2p0Fast,
|
||||
#[serde(rename = "sora_2")]
|
||||
Sora2,
|
||||
#[serde(rename = "sora_2_pro")]
|
||||
@@ -210,7 +208,6 @@ impl CommonModelType {
|
||||
Self::Seedance10Pro => "seedance_1p0_pro",
|
||||
Self::Seedance1p5Pro => "seedance_1p5_pro",
|
||||
Self::Seedance2p0 => "seedance_2p0",
|
||||
Self::Seedance2p0Fast => "seedance_2p0_fast",
|
||||
Self::Sora2 => "sora_2",
|
||||
Self::Sora2Pro => "sora_2_pro",
|
||||
Self::Veo2 => "veo_2",
|
||||
@@ -277,7 +274,6 @@ impl CommonModelType {
|
||||
"seedance_1p0_pro" => Ok(Self::Seedance10Pro),
|
||||
"seedance_1p5_pro" => Ok(Self::Seedance1p5Pro),
|
||||
"seedance_2p0" => Ok(Self::Seedance2p0),
|
||||
"seedance_2p0_fast" => Ok(Self::Seedance2p0Fast),
|
||||
"sora_2" => Ok(Self::Sora2),
|
||||
"sora_2_pro" => Ok(Self::Sora2Pro),
|
||||
"veo_2" => Ok(Self::Veo2),
|
||||
@@ -348,7 +344,6 @@ impl CommonModelType {
|
||||
Self::Seedance10Pro,
|
||||
Self::Seedance1p5Pro,
|
||||
Self::Seedance2p0,
|
||||
Self::Seedance2p0Fast,
|
||||
Self::Sora2,
|
||||
Self::Sora2Pro,
|
||||
Self::Veo2,
|
||||
@@ -416,7 +411,6 @@ impl CommonModelType {
|
||||
Self::Seedance10Pro => CommonModelClass::Video,
|
||||
Self::Seedance1p5Pro => CommonModelClass::Video,
|
||||
Self::Seedance2p0 => CommonModelClass::Video,
|
||||
Self::Seedance2p0Fast => CommonModelClass::Video,
|
||||
Self::Sora2 => CommonModelClass::Video,
|
||||
Self::Sora2Pro => CommonModelClass::Video,
|
||||
Self::Veo2 => CommonModelClass::Video,
|
||||
@@ -493,7 +487,6 @@ mod tests {
|
||||
assert_serialization(CommonModelType::Seedance10Pro, "seedance_1p0_pro");
|
||||
assert_serialization(CommonModelType::Seedance1p5Pro, "seedance_1p5_pro");
|
||||
assert_serialization(CommonModelType::Seedance2p0, "seedance_2p0");
|
||||
assert_serialization(CommonModelType::Seedance2p0Fast, "seedance_2p0_fast");
|
||||
assert_serialization(CommonModelType::Sora2, "sora_2");
|
||||
assert_serialization(CommonModelType::Sora2Pro, "sora_2_pro");
|
||||
assert_serialization(CommonModelType::Veo2, "veo_2");
|
||||
@@ -557,7 +550,6 @@ mod tests {
|
||||
assert_eq!(CommonModelType::Seedance10Pro.to_str(), "seedance_1p0_pro");
|
||||
assert_eq!(CommonModelType::Seedance1p5Pro.to_str(), "seedance_1p5_pro");
|
||||
assert_eq!(CommonModelType::Seedance2p0.to_str(), "seedance_2p0");
|
||||
assert_eq!(CommonModelType::Seedance2p0Fast.to_str(), "seedance_2p0_fast");
|
||||
assert_eq!(CommonModelType::Sora2.to_str(), "sora_2");
|
||||
assert_eq!(CommonModelType::Sora2Pro.to_str(), "sora_2_pro");
|
||||
assert_eq!(CommonModelType::Veo2.to_str(), "veo_2");
|
||||
@@ -621,7 +613,6 @@ mod tests {
|
||||
assert_eq!(CommonModelType::from_str("seedance_1p0_pro").unwrap(), CommonModelType::Seedance10Pro);
|
||||
assert_eq!(CommonModelType::from_str("seedance_1p5_pro").unwrap(), CommonModelType::Seedance1p5Pro);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0").unwrap(), CommonModelType::Seedance2p0);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_fast").unwrap(), CommonModelType::Seedance2p0Fast);
|
||||
assert_eq!(CommonModelType::from_str("sora_2").unwrap(), CommonModelType::Sora2);
|
||||
assert_eq!(CommonModelType::from_str("sora_2_pro").unwrap(), CommonModelType::Sora2Pro);
|
||||
assert_eq!(CommonModelType::from_str("veo_2").unwrap(), CommonModelType::Veo2);
|
||||
@@ -642,7 +633,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = CommonModelType::all_variants();
|
||||
assert_eq!(variants.len(), 55);
|
||||
assert_eq!(variants.len(), 54);
|
||||
// Image models
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Dev));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Schnell));
|
||||
@@ -687,7 +678,6 @@ mod tests {
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance10Pro));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance1p5Pro));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0Fast));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Sora2));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Sora2Pro));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Veo2));
|
||||
|
||||
@@ -38,9 +38,6 @@ pub enum CommonVideoModel {
|
||||
#[serde(rename = "seedance_2p0")]
|
||||
Seedance2p0,
|
||||
|
||||
#[serde(rename = "seedance_2p0_fast")]
|
||||
Seedance2p0Fast,
|
||||
|
||||
#[serde(rename = "sora_2")]
|
||||
Sora2,
|
||||
|
||||
@@ -78,7 +75,6 @@ impl CommonVideoModel {
|
||||
Self::Seedance10Lite => CommonModelType::Seedance10Lite,
|
||||
Self::Seedance1p5Pro => CommonModelType::Seedance1p5Pro,
|
||||
Self::Seedance2p0 => CommonModelType::Seedance2p0,
|
||||
Self::Seedance2p0Fast => CommonModelType::Seedance2p0Fast,
|
||||
Self::Sora2 => CommonModelType::Sora2,
|
||||
Self::Sora2Pro => CommonModelType::Sora2Pro,
|
||||
Self::Veo2 => CommonModelType::Veo2,
|
||||
@@ -109,7 +105,6 @@ mod tests {
|
||||
(CommonVideoModel::Seedance10Lite, CommonModelType::Seedance10Lite),
|
||||
(CommonVideoModel::Seedance1p5Pro, CommonModelType::Seedance1p5Pro),
|
||||
(CommonVideoModel::Seedance2p0, CommonModelType::Seedance2p0),
|
||||
(CommonVideoModel::Seedance2p0Fast, CommonModelType::Seedance2p0Fast),
|
||||
(CommonVideoModel::Sora2, CommonModelType::Sora2),
|
||||
(CommonVideoModel::Sora2Pro, CommonModelType::Sora2Pro),
|
||||
(CommonVideoModel::Veo2, CommonModelType::Veo2),
|
||||
|
||||
@@ -4,7 +4,6 @@ pub mod common_image_model;
|
||||
pub mod common_quality;
|
||||
pub mod common_model_class;
|
||||
pub mod common_model_type;
|
||||
pub mod model_creator;
|
||||
pub mod common_resolution;
|
||||
pub mod common_splat_model;
|
||||
pub mod common_video_model;
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
|
||||
/// The company or organization that created a model.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
|
||||
pub enum ModelCreator {
|
||||
#[serde(rename = "alibaba")]
|
||||
Alibaba,
|
||||
#[serde(rename = "artcraft")]
|
||||
ArtCraft,
|
||||
#[serde(rename = "black_forest_labs")]
|
||||
BlackForestLabs,
|
||||
#[serde(rename = "bytedance")]
|
||||
Bytedance,
|
||||
#[serde(rename = "fal")]
|
||||
Fal,
|
||||
#[serde(rename = "google")]
|
||||
Google,
|
||||
#[serde(rename = "grok")]
|
||||
Grok,
|
||||
#[serde(rename = "hailuo")]
|
||||
Hailuo,
|
||||
#[serde(rename = "higgsfield")]
|
||||
Higgsfield,
|
||||
#[serde(rename = "kling")]
|
||||
Kling,
|
||||
#[serde(rename = "krea")]
|
||||
Krea,
|
||||
#[serde(rename = "midjourney")]
|
||||
Midjourney,
|
||||
#[serde(rename = "open_ai")]
|
||||
OpenAi,
|
||||
#[serde(rename = "open_art")]
|
||||
OpenArt,
|
||||
#[serde(rename = "recraft")]
|
||||
Recraft,
|
||||
#[serde(rename = "replicate")]
|
||||
Replicate,
|
||||
#[serde(rename = "runway")]
|
||||
Runway,
|
||||
#[serde(rename = "stability")]
|
||||
Stability,
|
||||
#[serde(rename = "tencent")]
|
||||
Tencent,
|
||||
#[serde(rename = "tensor_art")]
|
||||
TensorArt,
|
||||
#[serde(rename = "vidu")]
|
||||
Vidu,
|
||||
#[serde(rename = "world_labs")]
|
||||
WorldLabs,
|
||||
}
|
||||
|
||||
impl ModelCreator {
|
||||
/// Returns the properly formatted human-readable name.
|
||||
pub fn get_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Alibaba => "Alibaba",
|
||||
Self::ArtCraft => "ArtCraft",
|
||||
Self::BlackForestLabs => "Black Forest Labs",
|
||||
Self::Bytedance => "ByteDance",
|
||||
Self::Fal => "fal",
|
||||
Self::Google => "Google",
|
||||
Self::Grok => "Grok",
|
||||
Self::Hailuo => "Hailuo",
|
||||
Self::Higgsfield => "Higgsfield",
|
||||
Self::Kling => "Kling",
|
||||
Self::Krea => "Krea",
|
||||
Self::Midjourney => "Midjourney",
|
||||
Self::OpenAi => "OpenAI",
|
||||
Self::OpenArt => "OpenArt",
|
||||
Self::Recraft => "Recraft",
|
||||
Self::Replicate => "Replicate",
|
||||
Self::Runway => "Runway",
|
||||
Self::Stability => "Stability AI",
|
||||
Self::Tencent => "Tencent",
|
||||
Self::TensorArt => "TensorArt",
|
||||
Self::Vidu => "Vidu",
|
||||
Self::WorldLabs => "World Labs",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Alibaba => "alibaba",
|
||||
Self::ArtCraft => "artcraft",
|
||||
Self::BlackForestLabs => "black_forest_labs",
|
||||
Self::Bytedance => "bytedance",
|
||||
Self::Fal => "fal",
|
||||
Self::Google => "google",
|
||||
Self::Grok => "grok",
|
||||
Self::Hailuo => "hailuo",
|
||||
Self::Higgsfield => "higgsfield",
|
||||
Self::Kling => "kling",
|
||||
Self::Krea => "krea",
|
||||
Self::Midjourney => "midjourney",
|
||||
Self::OpenAi => "open_ai",
|
||||
Self::OpenArt => "open_art",
|
||||
Self::Recraft => "recraft",
|
||||
Self::Replicate => "replicate",
|
||||
Self::Runway => "runway",
|
||||
Self::Stability => "stability",
|
||||
Self::Tencent => "tencent",
|
||||
Self::TensorArt => "tensor_art",
|
||||
Self::Vidu => "vidu",
|
||||
Self::WorldLabs => "world_labs",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"alibaba" => Ok(Self::Alibaba),
|
||||
"artcraft" => Ok(Self::ArtCraft),
|
||||
"black_forest_labs" => Ok(Self::BlackForestLabs),
|
||||
"bytedance" => Ok(Self::Bytedance),
|
||||
"fal" => Ok(Self::Fal),
|
||||
"google" => Ok(Self::Google),
|
||||
"grok" => Ok(Self::Grok),
|
||||
"hailuo" => Ok(Self::Hailuo),
|
||||
"higgsfield" => Ok(Self::Higgsfield),
|
||||
"kling" => Ok(Self::Kling),
|
||||
"krea" => Ok(Self::Krea),
|
||||
"midjourney" => Ok(Self::Midjourney),
|
||||
"open_ai" => Ok(Self::OpenAi),
|
||||
"open_art" => Ok(Self::OpenArt),
|
||||
"recraft" => Ok(Self::Recraft),
|
||||
"replicate" => Ok(Self::Replicate),
|
||||
"runway" => Ok(Self::Runway),
|
||||
"stability" => Ok(Self::Stability),
|
||||
"tencent" => Ok(Self::Tencent),
|
||||
"tensor_art" => Ok(Self::TensorArt),
|
||||
"vidu" => Ok(Self::Vidu),
|
||||
"world_labs" => Ok(Self::WorldLabs),
|
||||
_ => Err(format!("invalid ModelCreator value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
BTreeSet::from([
|
||||
Self::Alibaba,
|
||||
Self::ArtCraft,
|
||||
Self::BlackForestLabs,
|
||||
Self::Bytedance,
|
||||
Self::Fal,
|
||||
Self::Google,
|
||||
Self::Grok,
|
||||
Self::Hailuo,
|
||||
Self::Higgsfield,
|
||||
Self::Kling,
|
||||
Self::Krea,
|
||||
Self::Midjourney,
|
||||
Self::OpenAi,
|
||||
Self::OpenArt,
|
||||
Self::Recraft,
|
||||
Self::Replicate,
|
||||
Self::Runway,
|
||||
Self::Stability,
|
||||
Self::Tencent,
|
||||
Self::TensorArt,
|
||||
Self::Vidu,
|
||||
Self::WorldLabs,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(ModelCreator);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::common::generation::model_creator::ModelCreator;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod explicit_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(ModelCreator::Alibaba, "alibaba");
|
||||
assert_serialization(ModelCreator::ArtCraft, "artcraft");
|
||||
assert_serialization(ModelCreator::BlackForestLabs, "black_forest_labs");
|
||||
assert_serialization(ModelCreator::Bytedance, "bytedance");
|
||||
assert_serialization(ModelCreator::Fal, "fal");
|
||||
assert_serialization(ModelCreator::Google, "google");
|
||||
assert_serialization(ModelCreator::Grok, "grok");
|
||||
assert_serialization(ModelCreator::Hailuo, "hailuo");
|
||||
assert_serialization(ModelCreator::Higgsfield, "higgsfield");
|
||||
assert_serialization(ModelCreator::Kling, "kling");
|
||||
assert_serialization(ModelCreator::Krea, "krea");
|
||||
assert_serialization(ModelCreator::Midjourney, "midjourney");
|
||||
assert_serialization(ModelCreator::OpenAi, "open_ai");
|
||||
assert_serialization(ModelCreator::OpenArt, "open_art");
|
||||
assert_serialization(ModelCreator::Recraft, "recraft");
|
||||
assert_serialization(ModelCreator::Replicate, "replicate");
|
||||
assert_serialization(ModelCreator::Runway, "runway");
|
||||
assert_serialization(ModelCreator::Stability, "stability");
|
||||
assert_serialization(ModelCreator::Tencent, "tencent");
|
||||
assert_serialization(ModelCreator::TensorArt, "tensor_art");
|
||||
assert_serialization(ModelCreator::Vidu, "vidu");
|
||||
assert_serialization(ModelCreator::WorldLabs, "world_labs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(ModelCreator::Alibaba.to_str(), "alibaba");
|
||||
assert_eq!(ModelCreator::ArtCraft.to_str(), "artcraft");
|
||||
assert_eq!(ModelCreator::BlackForestLabs.to_str(), "black_forest_labs");
|
||||
assert_eq!(ModelCreator::OpenAi.to_str(), "open_ai");
|
||||
assert_eq!(ModelCreator::WorldLabs.to_str(), "world_labs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(ModelCreator::from_str("alibaba").unwrap(), ModelCreator::Alibaba);
|
||||
assert_eq!(ModelCreator::from_str("artcraft").unwrap(), ModelCreator::ArtCraft);
|
||||
assert_eq!(ModelCreator::from_str("black_forest_labs").unwrap(), ModelCreator::BlackForestLabs);
|
||||
assert_eq!(ModelCreator::from_str("open_ai").unwrap(), ModelCreator::OpenAi);
|
||||
assert_eq!(ModelCreator::from_str("world_labs").unwrap(), ModelCreator::WorldLabs);
|
||||
assert!(ModelCreator::from_str("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_name() {
|
||||
assert_eq!(ModelCreator::Alibaba.get_name(), "Alibaba");
|
||||
assert_eq!(ModelCreator::ArtCraft.get_name(), "ArtCraft");
|
||||
assert_eq!(ModelCreator::BlackForestLabs.get_name(), "Black Forest Labs");
|
||||
assert_eq!(ModelCreator::Bytedance.get_name(), "ByteDance");
|
||||
assert_eq!(ModelCreator::Fal.get_name(), "fal");
|
||||
assert_eq!(ModelCreator::Google.get_name(), "Google");
|
||||
assert_eq!(ModelCreator::OpenAi.get_name(), "OpenAI");
|
||||
assert_eq!(ModelCreator::Stability.get_name(), "Stability AI");
|
||||
assert_eq!(ModelCreator::WorldLabs.get_name(), "World Labs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
const EXPECTED_COUNT: usize = 22;
|
||||
assert_eq!(ModelCreator::all_variants().len(), EXPECTED_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(ModelCreator::all_variants().len(), ModelCreator::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in ModelCreator::all_variants() {
|
||||
assert_eq!(variant, ModelCreator::from_str(variant.to_str()).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
const MAX_LENGTH: usize = 32;
|
||||
for variant in ModelCreator::all_variants() {
|
||||
let serialized = variant.to_str();
|
||||
assert!(!serialized.is_empty());
|
||||
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long for VARCHAR({})", variant, MAX_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_variant_has_a_human_name() {
|
||||
for variant in ModelCreator::all_variants() {
|
||||
let name = variant.get_name();
|
||||
assert!(!name.is_empty(), "variant {:?} has empty name", variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,3 @@ ENABLE_PAGING=true
|
||||
# Doesn't make sense for jobs:
|
||||
# ENABLE_PAGING_FOR_500S=true
|
||||
|
||||
# We don't need to look back that far in development
|
||||
MAX_JOB_AGE_THRESHOLD_HOURS=5
|
||||
|
||||
# Don't collide with storyteller-web in development
|
||||
HTTP_BIND_ADDRESS=0.0.0.0:11223
|
||||
|
||||
|
||||
@@ -2,6 +2,3 @@
|
||||
|
||||
# Development
|
||||
SERVER_ENVIRONMENT='development'
|
||||
|
||||
# Don't collide with storyteller-web in development
|
||||
HTTP_BIND_ADDRESS=0.0.0.0:13333
|
||||
|
||||
@@ -9,8 +9,8 @@ COOKIE_DOMAIN=.jungle.horse
|
||||
WEBSITE_HOMEPAGE_REDIRECT=http://jungle.horse
|
||||
|
||||
# Paging
|
||||
ENABLE_PAGING=false
|
||||
ENABLE_PAGING_FOR_500S=false
|
||||
ENABLE_PAGING=true
|
||||
ENABLE_PAGING_FOR_500S=true
|
||||
|
||||
# Test the rate limiter bypass (obviously these are dummy values)
|
||||
AI_STREAMER_USERNAMES="PewDiePie,xQc"
|
||||
|
||||
@@ -2,7 +2,6 @@ use artcraft_api_defs::omni_gen::models::omni_gen_image_models::{OmniGenImageMod
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_image_model::CommonImageModel;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use once_cell::sync::Lazy;
|
||||
use enums::common::generation::common_quality::CommonQuality;
|
||||
@@ -22,8 +21,7 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::Flux1Dev,
|
||||
model_creator: Some(ModelCreator::BlackForestLabs),
|
||||
full_name: Some("FLUX.1 [dev]".to_string()),
|
||||
full_name: None,
|
||||
text_prompt_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::SquareHd,
|
||||
@@ -42,8 +40,7 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::Flux1Schnell,
|
||||
model_creator: Some(ModelCreator::BlackForestLabs),
|
||||
full_name: Some("FLUX.1 [schnell]".to_string()),
|
||||
full_name: None,
|
||||
text_prompt_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::SquareHd,
|
||||
@@ -62,7 +59,6 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::FluxPro11,
|
||||
model_creator: Some(ModelCreator::BlackForestLabs),
|
||||
full_name: Some("FLUX 1.1 [pro]".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
@@ -82,7 +78,6 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::FluxPro11Ultra,
|
||||
model_creator: Some(ModelCreator::BlackForestLabs),
|
||||
full_name: Some("FLUX 1.1 [pro] ultra".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
@@ -105,8 +100,7 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::NanoBanana, // NB: currently Gemini25Flash in our system
|
||||
model_creator: Some(ModelCreator::ArtCraft),
|
||||
full_name: Some("Nano Banana".to_string()),
|
||||
full_name: None,
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
@@ -132,8 +126,7 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::NanoBanana2,
|
||||
model_creator: Some(ModelCreator::ArtCraft),
|
||||
full_name: Some("Nano Banana 2".to_string()),
|
||||
full_name: None,
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
@@ -166,8 +159,7 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
model_creator: Some(ModelCreator::ArtCraft),
|
||||
full_name: Some("Nano Banana Pro".to_string()),
|
||||
full_name: None,
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
@@ -199,7 +191,6 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::GptImage1,
|
||||
model_creator: Some(ModelCreator::OpenAi),
|
||||
full_name: Some("GPT Image 1".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
@@ -223,7 +214,6 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::GptImage1p5,
|
||||
model_creator: Some(ModelCreator::OpenAi),
|
||||
full_name: Some("GPT Image 1.5".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
@@ -247,7 +237,6 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::Seedream4,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedream 4".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
@@ -272,7 +261,6 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::Seedream4p5,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedream 4.5".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
@@ -296,7 +284,6 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::Seedream5Lite,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedream 5 Lite".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
|
||||
@@ -2,7 +2,6 @@ use artcraft_api_defs::omni_gen::models::omni_gen_video_models::{OmniGenVideoMod
|
||||
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;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
@@ -21,8 +20,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance1p5Pro,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 1.5 Pro".to_string()),
|
||||
full_name: None,
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
@@ -51,8 +49,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0".to_string()),
|
||||
full_name: None,
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
@@ -83,47 +80,10 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0Fast,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Fast".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(true),
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// 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()),
|
||||
full_name: Some("Google 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,
|
||||
@@ -137,19 +97,16 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
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()),
|
||||
full_name: Some("Google Veo 3".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
CommonAspectRatio::Auto, // TODO: Only for image-to-video
|
||||
CommonAspectRatio::Square,
|
||||
]),
|
||||
// TODO: image-to-video aspect ratio options
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
@@ -164,8 +121,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Veo3Fast,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 3 Fast".to_string()),
|
||||
full_name: Some("Google Veo 3 Fast".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
@@ -181,10 +137,8 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo3p1,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 3.1".to_string()),
|
||||
full_name: Some("Google Veo 3.1".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
@@ -207,10 +161,8 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo3p1Fast,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 3.1 Fast".to_string()),
|
||||
full_name: Some("Google Veo 3.1 Fast".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
@@ -233,9 +185,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
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),
|
||||
@@ -251,9 +201,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
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),
|
||||
@@ -269,9 +217,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
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![
|
||||
@@ -286,9 +232,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling2p5TurboPro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.5 Turbo Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
@@ -305,9 +249,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling2p6Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.6 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
@@ -324,9 +266,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling3p0Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 3.0 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
@@ -345,9 +285,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling3p0Standard,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 3.0 Standard".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
@@ -366,9 +304,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
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),
|
||||
@@ -394,9 +330,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
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),
|
||||
@@ -413,9 +347,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
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),
|
||||
|
||||
+1
-97
@@ -36,25 +36,13 @@ use anyhow::anyhow;
|
||||
pub enum AdvancedCommonWebError {
|
||||
/// 400 Bad Request with a user-facing message.
|
||||
BadInputWithSimpleMessage(String),
|
||||
|
||||
/// 401 Unauthorized.
|
||||
NotAuthorized,
|
||||
|
||||
/// 404 Not Found.
|
||||
NotFound,
|
||||
|
||||
/// 402 Payment Required.
|
||||
PaymentRequired,
|
||||
|
||||
/// 403 Forbidden.
|
||||
Forbidden,
|
||||
|
||||
/// 403 Forbidden — content was rejected by a content policy filter (e.g. NSFW).
|
||||
ContentPolicyRejected,
|
||||
|
||||
/// 403 Forbidden — content was rejected, with a user-facing message.
|
||||
ContentPolicyRejectedWithMessage(String),
|
||||
|
||||
/// Uncaught errors are always 500 Internal Server Error.
|
||||
/// The user will never see the error cause or message, but our
|
||||
/// middleware will handle alerting, logging, etc.
|
||||
@@ -62,12 +50,6 @@ pub enum AdvancedCommonWebError {
|
||||
/// Stored in `Arc` so the error alerting middleware can clone it for paging
|
||||
/// without consuming the original.
|
||||
UncaughtServerError(Arc<dyn std::error::Error + Send + Sync + 'static>),
|
||||
|
||||
/// Same as above, with a specified extra message string
|
||||
UncaughtServerErrorWithInternalMessage {
|
||||
internal_message: String,
|
||||
error: Arc<dyn std::error::Error + Send + Sync + 'static>
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Public accessors ===============
|
||||
@@ -78,13 +60,6 @@ impl AdvancedCommonWebError {
|
||||
Self::UncaughtServerError(Arc::new(error))
|
||||
}
|
||||
|
||||
pub fn from_error_with_message(message: String, error: impl std::error::Error + Send + Sync + 'static) -> Self {
|
||||
Self::UncaughtServerErrorWithInternalMessage {
|
||||
internal_message: message,
|
||||
error: Arc::new(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap an `anyhow::Error` as an `UncaughtServerError`.
|
||||
/// (`anyhow::Error` doesn't implement `std::error::Error`, so `from_error` can't accept it.)
|
||||
pub fn from_anyhow_error(error: anyhow::Error) -> Self {
|
||||
@@ -100,7 +75,6 @@ impl AdvancedCommonWebError {
|
||||
pub fn cause(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> {
|
||||
match self {
|
||||
Self::UncaughtServerError(err) => Some(err.as_ref()),
|
||||
Self::UncaughtServerErrorWithInternalMessage { error, .. } => Some(error.as_ref()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -110,18 +84,13 @@ impl AdvancedCommonWebError {
|
||||
pub fn clone_cause_arc(&self) -> Option<Arc<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
match self {
|
||||
Self::UncaughtServerError(err) => Some(Arc::clone(err)),
|
||||
Self::UncaughtServerErrorWithInternalMessage { error, .. } => Some(Arc::clone(error)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this is a server error (500).
|
||||
pub fn is_server_error(&self) -> bool {
|
||||
match self {
|
||||
Self::UncaughtServerError(_) => true,
|
||||
Self::UncaughtServerErrorWithInternalMessage { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self, Self::UncaughtServerError(_))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,13 +103,7 @@ impl Display for AdvancedCommonWebError {
|
||||
Self::NotAuthorized => write!(f, "Not authorized"),
|
||||
Self::NotFound => write!(f, "Not found"),
|
||||
Self::PaymentRequired => write!(f, "Payment required"),
|
||||
Self::Forbidden => write!(f, "Forbidden"),
|
||||
Self::ContentPolicyRejected => write!(f, "Content policy rejected"),
|
||||
Self::ContentPolicyRejectedWithMessage(msg) => write!(f, "Content policy rejected: {}", msg),
|
||||
Self::UncaughtServerError(err) => write!(f, "Server error: {}", err),
|
||||
Self::UncaughtServerErrorWithInternalMessage { internal_message, error } => {
|
||||
write!(f, "Server error: {}: {}", internal_message, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,13 +115,7 @@ impl std::fmt::Debug for AdvancedCommonWebError {
|
||||
Self::NotAuthorized => write!(f, "NotAuthorized"),
|
||||
Self::NotFound => write!(f, "NotFound"),
|
||||
Self::PaymentRequired => write!(f, "PaymentRequired"),
|
||||
Self::Forbidden => write!(f, "Forbidden"),
|
||||
Self::ContentPolicyRejected => write!(f, "ContentPolicyRejected"),
|
||||
Self::ContentPolicyRejectedWithMessage(msg) => write!(f, "ContentPolicyRejectedWithMessage({:?})", msg),
|
||||
Self::UncaughtServerError(err) => write!(f, "UncaughtServerError({:?})", err),
|
||||
Self::UncaughtServerErrorWithInternalMessage { internal_message, error } => {
|
||||
write!(f, "UncaughtServerErrorWithInternalMessage({:?}, {:?})", internal_message, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,7 +124,6 @@ impl std::error::Error for AdvancedCommonWebError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::UncaughtServerError(err) => Some(err.as_ref()),
|
||||
Self::UncaughtServerErrorWithInternalMessage { error, .. } => Some(error.as_ref()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -182,11 +138,7 @@ impl ResponseError for AdvancedCommonWebError {
|
||||
Self::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
Self::NotFound => StatusCode::NOT_FOUND,
|
||||
Self::PaymentRequired => StatusCode::PAYMENT_REQUIRED,
|
||||
Self::Forbidden => StatusCode::FORBIDDEN,
|
||||
Self::ContentPolicyRejected => StatusCode::FORBIDDEN,
|
||||
Self::ContentPolicyRejectedWithMessage(_) => StatusCode::FORBIDDEN,
|
||||
Self::UncaughtServerError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::UncaughtServerErrorWithInternalMessage { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,24 +155,6 @@ impl ResponseError for AdvancedCommonWebError {
|
||||
message: msg,
|
||||
})
|
||||
}
|
||||
Self::ContentPolicyRejected => {
|
||||
HttpResponseBuilder::new(status)
|
||||
.json(JsonErrorWithMessage {
|
||||
success: false,
|
||||
error_code: status.as_u16(),
|
||||
error_code_str: status.canonical_reason(),
|
||||
message: "Content rejected by policy",
|
||||
})
|
||||
}
|
||||
Self::ContentPolicyRejectedWithMessage(msg) => {
|
||||
HttpResponseBuilder::new(status)
|
||||
.json(JsonErrorWithMessage {
|
||||
success: false,
|
||||
error_code: status.as_u16(),
|
||||
error_code_str: status.canonical_reason(),
|
||||
message: msg,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
HttpResponseBuilder::new(status)
|
||||
.json(JsonErrorWithoutMessage {
|
||||
@@ -498,35 +432,5 @@ mod tests {
|
||||
assert!(std::error::Error::source(&AdvancedCommonWebError::NotFound).is_none());
|
||||
assert!(std::error::Error::source(&AdvancedCommonWebError::NotAuthorized).is_none());
|
||||
assert!(std::error::Error::source(&AdvancedCommonWebError::PaymentRequired).is_none());
|
||||
assert!(std::error::Error::source(&AdvancedCommonWebError::Forbidden).is_none());
|
||||
assert!(std::error::Error::source(&AdvancedCommonWebError::ContentPolicyRejected).is_none());
|
||||
assert!(std::error::Error::source(&AdvancedCommonWebError::ContentPolicyRejectedWithMessage("test".to_string())).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forbidden_returns_403() {
|
||||
let error = AdvancedCommonWebError::Forbidden;
|
||||
assert_eq!(error.status_code(), StatusCode::FORBIDDEN);
|
||||
assert!(!error.is_server_error());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_policy_rejected_returns_403() {
|
||||
let error = AdvancedCommonWebError::ContentPolicyRejected;
|
||||
assert_eq!(error.status_code(), StatusCode::FORBIDDEN);
|
||||
assert!(!error.is_server_error());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_policy_rejected_with_message_returns_403() {
|
||||
let error = AdvancedCommonWebError::ContentPolicyRejectedWithMessage("NSFW detected".to_string());
|
||||
assert_eq!(error.status_code(), StatusCode::FORBIDDEN);
|
||||
assert!(!error.is_server_error());
|
||||
|
||||
let response = error.error_response();
|
||||
let bytes = response.into_body().try_into_bytes().unwrap();
|
||||
let body = String::from_utf8(bytes.to_vec()).unwrap();
|
||||
assert!(body.contains("\"message\":\"NSFW detected\""));
|
||||
assert!(body.contains("\"error_code\":403"));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -114,10 +114,7 @@ pub async fn edit_character_handler(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error updating character on Kinovi: {:?}", err);
|
||||
AdvancedCommonWebError::from_error_with_message(
|
||||
"Error Updating Kinovi Character API".to_string(),
|
||||
err,
|
||||
)
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
info!("Updated character {} on Kinovi (name='{}')", kinovi_id, final_name);
|
||||
|
||||
-1
@@ -140,7 +140,6 @@ fn map_video_model(model: CommonVideoModel) -> Result<RouterVideoModel, HandlerE
|
||||
CommonVideoModel::Seedance10Lite => RouterVideoModel::Seedance10Lite,
|
||||
CommonVideoModel::Seedance1p5Pro => RouterVideoModel::Seedance1p5Pro,
|
||||
CommonVideoModel::Seedance2p0 => RouterVideoModel::Seedance2p0,
|
||||
CommonVideoModel::Seedance2p0Fast => RouterVideoModel::Seedance2p0Fast,
|
||||
CommonVideoModel::Sora2 => RouterVideoModel::Sora2,
|
||||
CommonVideoModel::Sora2Pro => RouterVideoModel::Sora2Pro,
|
||||
CommonVideoModel::Veo2 => RouterVideoModel::Veo2,
|
||||
|
||||
+14
-30
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoint_helpers::refund_wallet_after_api_failure::refund_wallet_after_api_failure;
|
||||
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
@@ -12,10 +12,10 @@ use actix_web::{web, HttpRequest};
|
||||
use artcraft_api_defs::generate::splat::generate_worldlabs_marble_0p1_mini_splat::{GenerateWorldlabsMarble0p1MiniSplatRequest, GenerateWorldlabsMarble0p1MiniSplatResponse};
|
||||
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::worldlabs::insert_generic_inference_job_for_worldlabs_queue_with_apriori_job_token::{insert_generic_inference_job_for_worldlabs_queue_with_apriori_job_token, InsertGenericInferenceForWorldlabsWithAprioriJobTokenArgs};
|
||||
@@ -28,8 +28,6 @@ use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel;
|
||||
use worldlabs_api_client::api::requests::generate_world::generate_world::{generate_world, GenerateWorldArgs};
|
||||
use worldlabs_api_client::api::requests::generate_world::http_request::{ContentReference, WorldPrompt};
|
||||
use worldlabs_api_client::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use worldlabs_api_client::error::world_labs_error::WorldLabsError;
|
||||
use worldlabs_api_client::error::world_labs_specific_api_error::WorldLabsSpecificApiError;
|
||||
use worldlabs_api_client::pricing::check_pricing::{calculate_cost, InputType};
|
||||
|
||||
/// World Labs Marble 0.1-mini Splat Generation
|
||||
@@ -48,18 +46,18 @@ pub async fn generate_worldlabs_marble_0p1_mini_splat_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<GenerateWorldlabsMarble0p1MiniSplatRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateWorldlabsMarble0p1MiniSplatResponse>, AdvancedCommonWebError> {
|
||||
) -> Result<Json<GenerateWorldlabsMarble0p1MiniSplatResponse>, CommonWebError> {
|
||||
|
||||
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
|
||||
|
||||
if request.image_media_file_token.is_none() && request.prompt.is_none() {
|
||||
return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
"At least one of image_media_file_token or prompt must be provided".to_string()
|
||||
));
|
||||
}
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(reason));
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
@@ -72,7 +70,7 @@ pub async fn generate_worldlabs_marble_0p1_mini_splat_handler(
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
AdvancedCommonWebError::from(e)
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
@@ -82,7 +80,7 @@ pub async fn generate_worldlabs_marble_0p1_mini_splat_handler(
|
||||
let user_token = match maybe_user_session.as_ref() {
|
||||
Some(session) => &session.user_token,
|
||||
None => {
|
||||
return Err(AdvancedCommonWebError::NotAuthorized);
|
||||
return Err(CommonWebError::NotAuthorized);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -90,7 +88,7 @@ pub async fn generate_worldlabs_marble_0p1_mini_splat_handler(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
|
||||
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
|
||||
})?;
|
||||
|
||||
let apriori_job_token = InferenceJobToken::generate();
|
||||
@@ -126,7 +124,7 @@ pub async fn generate_worldlabs_marble_0p1_mini_splat_handler(
|
||||
let cdn_url = image_urls.get(image_token)
|
||||
.ok_or_else(|| {
|
||||
warn!("Image token not found in lookup results: {:?}", image_token);
|
||||
AdvancedCommonWebError::server_error_with_message("Image token was not found in lookup results")
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
WorldPrompt::Image {
|
||||
@@ -159,7 +157,7 @@ pub async fn generate_worldlabs_marble_0p1_mini_splat_handler(
|
||||
Err(err) => {
|
||||
warn!("World Labs generate_world error: {:?}", err);
|
||||
refund_wallet_after_api_failure(&wallet_deduction.ledger_entry_token, &mut mysql_connection).await?;
|
||||
return Err(classify_worldlabs_error(err));
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -170,7 +168,7 @@ pub async fn generate_worldlabs_marble_0p1_mini_splat_handler(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error starting MySQL transaction: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let prompt_result = insert_prompt(InsertPromptArgs {
|
||||
@@ -239,7 +237,7 @@ pub async fn generate_worldlabs_marble_0p1_mini_splat_handler(
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting generic inference job for WorldLabs queue: {:?}", err);
|
||||
return Err(AdvancedCommonWebError::from_error(err));
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -248,7 +246,7 @@ pub async fn generate_worldlabs_marble_0p1_mini_splat_handler(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error committing MySQL transaction: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
Ok(Json(GenerateWorldlabsMarble0p1MiniSplatResponse {
|
||||
@@ -256,17 +254,3 @@ pub async fn generate_worldlabs_marble_0p1_mini_splat_handler(
|
||||
inference_job_token: job_token,
|
||||
}))
|
||||
}
|
||||
|
||||
fn classify_worldlabs_error(err: WorldLabsError) -> AdvancedCommonWebError {
|
||||
if let WorldLabsError::ApiSpecific(WorldLabsSpecificApiError::NsfwContentPolicyRejected { message }) = &err {
|
||||
return AdvancedCommonWebError::ContentPolicyRejectedWithMessage(
|
||||
message.clone().unwrap_or_else(|| "Content rejected by policy".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
if err.is_403_forbidden() {
|
||||
return AdvancedCommonWebError::Forbidden;
|
||||
}
|
||||
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
}
|
||||
|
||||
+14
-31
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoint_helpers::refund_wallet_after_api_failure::refund_wallet_after_api_failure;
|
||||
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
@@ -12,10 +12,10 @@ use actix_web::{web, HttpRequest};
|
||||
use artcraft_api_defs::generate::splat::generate_worldlabs_marble_0p1_plus_splat::{GenerateWorldlabsMarble0p1PlusSplatRequest, GenerateWorldlabsMarble0p1PlusSplatResponse};
|
||||
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::worldlabs::insert_generic_inference_job_for_worldlabs_queue_with_apriori_job_token::{insert_generic_inference_job_for_worldlabs_queue_with_apriori_job_token, InsertGenericInferenceForWorldlabsWithAprioriJobTokenArgs};
|
||||
@@ -28,8 +28,6 @@ use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel;
|
||||
use worldlabs_api_client::api::requests::generate_world::generate_world::{generate_world, GenerateWorldArgs};
|
||||
use worldlabs_api_client::api::requests::generate_world::http_request::{ContentReference, WorldPrompt};
|
||||
use worldlabs_api_client::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use worldlabs_api_client::error::world_labs_error::WorldLabsError;
|
||||
use worldlabs_api_client::error::world_labs_specific_api_error::WorldLabsSpecificApiError;
|
||||
use worldlabs_api_client::pricing::check_pricing::{calculate_cost, InputType};
|
||||
|
||||
/// World Labs Marble 0.1-plus Splat Generation
|
||||
@@ -48,18 +46,18 @@ pub async fn generate_worldlabs_marble_0p1_plus_splat_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<GenerateWorldlabsMarble0p1PlusSplatRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GenerateWorldlabsMarble0p1PlusSplatResponse>, AdvancedCommonWebError> {
|
||||
) -> Result<Json<GenerateWorldlabsMarble0p1PlusSplatResponse>, CommonWebError> {
|
||||
|
||||
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
|
||||
|
||||
if request.image_media_file_token.is_none() && request.prompt.is_none() {
|
||||
return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
"At least one of image_media_file_token or prompt must be provided".to_string()
|
||||
));
|
||||
}
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(reason));
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
@@ -72,7 +70,7 @@ pub async fn generate_worldlabs_marble_0p1_plus_splat_handler(
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
AdvancedCommonWebError::from(e)
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
@@ -82,7 +80,7 @@ pub async fn generate_worldlabs_marble_0p1_plus_splat_handler(
|
||||
let user_token = match maybe_user_session.as_ref() {
|
||||
Some(session) => &session.user_token,
|
||||
None => {
|
||||
return Err(AdvancedCommonWebError::NotAuthorized);
|
||||
return Err(CommonWebError::NotAuthorized);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -90,7 +88,7 @@ pub async fn generate_worldlabs_marble_0p1_plus_splat_handler(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
|
||||
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
|
||||
})?;
|
||||
|
||||
let apriori_job_token = InferenceJobToken::generate();
|
||||
@@ -126,7 +124,7 @@ pub async fn generate_worldlabs_marble_0p1_plus_splat_handler(
|
||||
let cdn_url = image_urls.get(image_token)
|
||||
.ok_or_else(|| {
|
||||
warn!("Image token not found in lookup results: {:?}", image_token);
|
||||
AdvancedCommonWebError::server_error_with_message("Image token no found in lookup result")
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
WorldPrompt::Image {
|
||||
@@ -159,7 +157,7 @@ pub async fn generate_worldlabs_marble_0p1_plus_splat_handler(
|
||||
Err(err) => {
|
||||
warn!("World Labs generate_world error: {:?}", err);
|
||||
refund_wallet_after_api_failure(&wallet_deduction.ledger_entry_token, &mut mysql_connection).await?;
|
||||
return Err(classify_worldlabs_error(err));
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -170,7 +168,7 @@ pub async fn generate_worldlabs_marble_0p1_plus_splat_handler(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error starting MySQL transaction: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let prompt_result = insert_prompt(InsertPromptArgs {
|
||||
@@ -239,7 +237,7 @@ pub async fn generate_worldlabs_marble_0p1_plus_splat_handler(
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting generic inference job for WorldLabs queue: {:?}", err);
|
||||
return Err(AdvancedCommonWebError::from_error(err));
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -248,7 +246,7 @@ pub async fn generate_worldlabs_marble_0p1_plus_splat_handler(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error committing MySQL transaction: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
Ok(Json(GenerateWorldlabsMarble0p1PlusSplatResponse {
|
||||
@@ -256,18 +254,3 @@ pub async fn generate_worldlabs_marble_0p1_plus_splat_handler(
|
||||
inference_job_token: job_token,
|
||||
}))
|
||||
}
|
||||
|
||||
fn classify_worldlabs_error(err: WorldLabsError) -> AdvancedCommonWebError {
|
||||
|
||||
if let WorldLabsError::ApiSpecific(WorldLabsSpecificApiError::NsfwContentPolicyRejected { message }) = &err {
|
||||
return AdvancedCommonWebError::ContentPolicyRejectedWithMessage(
|
||||
message.clone().unwrap_or_else(|| "Content rejected by policy".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
if err.is_403_forbidden() {
|
||||
return AdvancedCommonWebError::Forbidden;
|
||||
}
|
||||
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
}
|
||||
|
||||
+14
-4
@@ -148,9 +148,13 @@ pub async fn generate_veo_2_image_to_video_handler(
|
||||
.map(|prompt| prompt.trim())
|
||||
.unwrap_or_else(|| "");
|
||||
|
||||
if let Some(aspect_ratio) = &request.aspect_ratio {
|
||||
warn!("Aspect ratio is not read in Veo 2 image-to-video");
|
||||
}
|
||||
let aspect_ratio = match &request.aspect_ratio {
|
||||
Some(GenerateVeo2AspectRatio::Auto) => Veo2AspectRatio::Auto,
|
||||
Some(GenerateVeo2AspectRatio::AutoPreferPortrait) => Veo2AspectRatio::AutoPreferPortrait,
|
||||
Some(GenerateVeo2AspectRatio::WideSixteenNine) => Veo2AspectRatio::WideSixteenNine,
|
||||
Some(GenerateVeo2AspectRatio::TallNineSixteen) => Veo2AspectRatio::TallNineSixteen,
|
||||
None => Veo2AspectRatio::WideSixteenNine, // Default to 16:9
|
||||
};
|
||||
|
||||
let duration = match &request.duration {
|
||||
Some(GenerateVeo2Duration::FiveSeconds) => Veo2Duration::FiveSeconds,
|
||||
@@ -165,6 +169,7 @@ pub async fn generate_veo_2_image_to_video_handler(
|
||||
webhook_url: &server_state.fal.webhook_url,
|
||||
duration,
|
||||
prompt,
|
||||
aspect_ratio,
|
||||
api_key: &server_state.fal.api_key,
|
||||
};
|
||||
|
||||
@@ -217,7 +222,12 @@ pub async fn generate_veo_2_image_to_video_handler(
|
||||
maybe_negative_prompt: None,
|
||||
maybe_other_args: None,
|
||||
maybe_generation_mode: Some(CommonGenerationMode::Keyframe), // TODO: This endpoint only supports keyframes for now
|
||||
maybe_aspect_ratio: None, // NB: The aspect ratio is not read for Veo 2 image-to-video
|
||||
maybe_aspect_ratio: request.aspect_ratio.as_ref().map(|ar| match ar {
|
||||
GenerateVeo2AspectRatio::Auto => CommonAspectRatio::Auto,
|
||||
GenerateVeo2AspectRatio::AutoPreferPortrait => CommonAspectRatio::Auto,
|
||||
GenerateVeo2AspectRatio::WideSixteenNine => CommonAspectRatio::WideSixteenByNine,
|
||||
GenerateVeo2AspectRatio::TallNineSixteen => CommonAspectRatio::TallNineBySixteen,
|
||||
}),
|
||||
maybe_resolution: None,
|
||||
maybe_batch_count: None,
|
||||
maybe_generate_audio: None,
|
||||
|
||||
+3
-15
@@ -11,7 +11,7 @@ use crate::state::server_state::ServerState;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use anyhow::anyhow;
|
||||
use artcraft_api_defs::generate::video::generate_veo_3_fast_image_to_video::{GenerateVeo3FastAspectRatio, GenerateVeo3FastDuration, GenerateVeo3FastImageToVideoRequest, GenerateVeo3FastImageToVideoResponse, GenerateVeo3FastResolution};
|
||||
use artcraft_api_defs::generate::video::generate_veo_3_fast_image_to_video::{GenerateVeo3FastDuration, GenerateVeo3FastImageToVideoRequest, GenerateVeo3FastImageToVideoResponse, GenerateVeo3FastResolution};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
@@ -19,7 +19,7 @@ use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs, Veo3FastAspectRatio, Veo3FastDuration, Veo3FastResolution};
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs, Veo3FastDuration, Veo3FastResolution};
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
|
||||
@@ -141,13 +141,6 @@ pub async fn generate_veo_3_fast_image_to_video_handler(
|
||||
.map(|prompt| prompt.trim())
|
||||
.unwrap_or_else(|| "");
|
||||
|
||||
let aspect_ratio = match &request.aspect_ratio {
|
||||
Some(GenerateVeo3FastAspectRatio::Auto) => Veo3FastAspectRatio::Auto,
|
||||
Some(GenerateVeo3FastAspectRatio::SixteenByNine) => Veo3FastAspectRatio::WideSixteenNine,
|
||||
Some(GenerateVeo3FastAspectRatio::NineBySixteen) => Veo3FastAspectRatio::TallNineSixteen,
|
||||
None => Veo3FastAspectRatio::Auto,
|
||||
};
|
||||
|
||||
let resolution = match &request.resolution {
|
||||
Some(GenerateVeo3FastResolution::SevenTwentyP) => Veo3FastResolution::SevenTwentyP,
|
||||
Some(GenerateVeo3FastResolution::TenEightyP) => Veo3FastResolution::TenEightyP,
|
||||
@@ -165,7 +158,6 @@ pub async fn generate_veo_3_fast_image_to_video_handler(
|
||||
let args = Veo3FastArgs {
|
||||
image_url: media_links.cdn_url,
|
||||
prompt,
|
||||
aspect_ratio,
|
||||
duration,
|
||||
resolution,
|
||||
generate_audio,
|
||||
@@ -222,11 +214,7 @@ pub async fn generate_veo_3_fast_image_to_video_handler(
|
||||
maybe_negative_prompt: None,
|
||||
maybe_other_args: None,
|
||||
maybe_generation_mode: Some(CommonGenerationMode::Keyframe), // TODO: This endpoint only supports keyframes for now
|
||||
maybe_aspect_ratio: request.aspect_ratio.as_ref().map(|ar| match ar {
|
||||
GenerateVeo3FastAspectRatio::Auto => enums::common::generation::common_aspect_ratio::CommonAspectRatio::Auto,
|
||||
GenerateVeo3FastAspectRatio::SixteenByNine => enums::common::generation::common_aspect_ratio::CommonAspectRatio::WideSixteenByNine,
|
||||
GenerateVeo3FastAspectRatio::NineBySixteen => enums::common::generation::common_aspect_ratio::CommonAspectRatio::TallNineBySixteen,
|
||||
}),
|
||||
maybe_aspect_ratio: None,
|
||||
maybe_resolution: None,
|
||||
maybe_batch_count: None,
|
||||
maybe_generate_audio: Some(generate_audio),
|
||||
|
||||
+13
-14
@@ -14,13 +14,13 @@ use anyhow::anyhow;
|
||||
use artcraft_api_defs::generate::video::generate_veo_3_image_to_video::{GenerateVeo3AspectRatio, GenerateVeo3Duration, GenerateVeo3ImageToVideoRequest, GenerateVeo3ImageToVideoResponse, GenerateVeo3Resolution};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::visibility::Visibility;
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3I2vAspectRatio, Veo3I2vDuration, Veo3I2vResolution};
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3AspectRatio, Veo3Duration, Veo3Resolution};
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
|
||||
@@ -143,22 +143,21 @@ pub async fn generate_veo_3_image_to_video_handler(
|
||||
.unwrap_or_else(|| "");
|
||||
|
||||
let aspect_ratio = match &request.aspect_ratio {
|
||||
Some(GenerateVeo3AspectRatio::WideSixteenNine) => Veo3I2vAspectRatio::WideSixteenNine,
|
||||
Some(GenerateVeo3AspectRatio::TallNineSixteen) => Veo3I2vAspectRatio::TallNineSixteen,
|
||||
// Square is not supported by Veo 3 i2v — fall back to Auto.
|
||||
Some(GenerateVeo3AspectRatio::Square) => Veo3I2vAspectRatio::Auto,
|
||||
None => Veo3I2vAspectRatio::WideSixteenNine,
|
||||
Some(GenerateVeo3AspectRatio::WideSixteenNine) => Veo3AspectRatio::WideSixteenNine,
|
||||
Some(GenerateVeo3AspectRatio::TallNineSixteen) => Veo3AspectRatio::TallNineSixteen,
|
||||
Some(GenerateVeo3AspectRatio::Square) => Veo3AspectRatio::Square,
|
||||
None => Veo3AspectRatio::WideSixteenNine,
|
||||
};
|
||||
|
||||
let resolution = match &request.resolution {
|
||||
Some(GenerateVeo3Resolution::SevenTwentyP) => Veo3I2vResolution::SevenTwentyP,
|
||||
Some(GenerateVeo3Resolution::TenEightyP) => Veo3I2vResolution::TenEightyP,
|
||||
None => Veo3I2vResolution::Default,
|
||||
Some(GenerateVeo3Resolution::SevenTwentyP) => Veo3Resolution::SevenTwentyP,
|
||||
Some(GenerateVeo3Resolution::TenEightyP) => Veo3Resolution::TenEightyP,
|
||||
None => Veo3Resolution::Default,
|
||||
};
|
||||
|
||||
let duration = match &request.duration {
|
||||
Some(GenerateVeo3Duration::EightSeconds) => Veo3I2vDuration::EightSeconds,
|
||||
None => Veo3I2vDuration::EightSeconds,
|
||||
Some(GenerateVeo3Duration::EightSeconds) => Veo3Duration::EightSeconds,
|
||||
None => Veo3Duration::EightSeconds,
|
||||
};
|
||||
|
||||
let generate_audio = request.generate_audio
|
||||
|
||||
+6
-29
@@ -1,4 +1,3 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
@@ -6,14 +5,11 @@ use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::moderation::alerts::moderation_send_alert::{ModerationSendAlertRequest, ModerationSendAlertResponse, ModerationSendAlertUrgency};
|
||||
use enums::by_table::staff_audit_logs::staff_audit_action::StaffAuditAction;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::staff_audit_logs::insert_staff_audit_log::{
|
||||
insert_staff_audit_log, InsertStaffAuditLogArgs,
|
||||
};
|
||||
use pager::client::pager::Pager;
|
||||
use pager::notification::notification_details::NotificationDetails;
|
||||
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
|
||||
use pager::notification::notification_urgency::NotificationUrgency;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
@@ -33,16 +29,11 @@ pub async fn moderation_send_alert_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<ModerationSendAlertRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ModerationSendAlertResponse>, AdvancedCommonWebError> {
|
||||
) -> Result<Json<ModerationSendAlertResponse>, CommonWebError> {
|
||||
|
||||
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Moderator check failed: {:?}", err);
|
||||
AdvancedCommonWebError::NotAuthorized
|
||||
})?;
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let title = request.title.clone()
|
||||
.unwrap_or_else(|| "Moderation Alert".to_string());
|
||||
@@ -77,23 +68,9 @@ pub async fn moderation_send_alert_handler(
|
||||
.enqueue_page(notification)
|
||||
.map_err(|err| {
|
||||
warn!("moderation_send_alert error: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
// Insert staff audit log.
|
||||
let _audit_token = insert_staff_audit_log(InsertStaffAuditLogArgs {
|
||||
audit_action: StaffAuditAction::SendAlert,
|
||||
maybe_entity_type: None,
|
||||
maybe_entity_token: None,
|
||||
staff_user_token: &user_session.user_token,
|
||||
actor_ip_address: &ip_address,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("Failed to insert staff audit log: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
Ok(Json(ModerationSendAlertResponse {
|
||||
success: true,
|
||||
}))
|
||||
|
||||
+8
-41
@@ -1,26 +1,19 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::{info, warn};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::moderation::wallets::moderator_add_banked_balance_to_wallet::{
|
||||
ModeratorAddBankedBalanceToWalletPathInfo,
|
||||
ModeratorAddBankedBalanceToWalletRequest,
|
||||
ModeratorAddBankedBalanceToWalletResponse,
|
||||
};
|
||||
use enums::by_table::staff_audit_logs::staff_audit_action::StaffAuditAction;
|
||||
use enums::by_table::staff_audit_logs::staff_audit_entity_type::StaffAuditEntityType;
|
||||
use enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::staff_audit_logs::insert_staff_audit_log::{
|
||||
insert_staff_audit_log, InsertStaffAuditLogArgs,
|
||||
};
|
||||
use mysql_queries::queries::wallets::add_durable_banked_balance_to_wallet::add_durable_banked_balance_to_wallet;
|
||||
|
||||
use tokens::tokens::wallets::WalletToken;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
@@ -44,29 +37,17 @@ pub async fn moderator_add_banked_balance_to_wallet_handler(
|
||||
path: Path<ModeratorAddBankedBalanceToWalletPathInfo>,
|
||||
request: Json<ModeratorAddBankedBalanceToWalletRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ModeratorAddBankedBalanceToWalletResponse>, AdvancedCommonWebError> {
|
||||
) -> Result<Json<ModeratorAddBankedBalanceToWalletResponse>, CommonWebError> {
|
||||
|
||||
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
|
||||
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Moderator check failed: {:?}", err);
|
||||
AdvancedCommonWebError::NotAuthorized
|
||||
})?;
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
info!(
|
||||
"Moderator {} adding {} banked credits to wallet {}",
|
||||
user_session.user_token.as_str(),
|
||||
request.credits,
|
||||
path.wallet_token.as_str(),
|
||||
);
|
||||
.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let mut transaction = server_state.mysql_pool.begin()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("moderator_add_banked_balance_to_wallet transaction begin error: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let _result = add_durable_banked_balance_to_wallet(
|
||||
@@ -79,28 +60,14 @@ pub async fn moderator_add_banked_balance_to_wallet_handler(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("moderator_add_banked_balance_to_wallet error: {:?}", err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
// Insert staff audit log.
|
||||
let _audit_token = insert_staff_audit_log(InsertStaffAuditLogArgs {
|
||||
audit_action: StaffAuditAction::AddWalletBankedBalance,
|
||||
maybe_entity_type: Some(StaffAuditEntityType::Wallet),
|
||||
maybe_entity_token: Some(path.wallet_token.as_str()),
|
||||
staff_user_token: &user_session.user_token,
|
||||
actor_ip_address: &ip_address,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("Failed to insert staff audit log: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
transaction.commit()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("moderator_add_banked_balance_to_wallet commit error: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
Ok(Json(ModeratorAddBankedBalanceToWalletResponse {
|
||||
|
||||
+5
@@ -134,6 +134,9 @@ pub async fn omni_gen_image_generate_handler(
|
||||
|
||||
let distilled = distill_image_request(&request, media_file_hydration_map.as_ref())?;
|
||||
|
||||
info!(">>> Distilled cost: {:?}", distilled.cost);
|
||||
info!(">>> Distilled plan: {:?}", distilled.plan());
|
||||
|
||||
// ==================== BILLING ==================== //
|
||||
|
||||
let cost = distilled.cost.cost_in_credits.unwrap_or(0);
|
||||
@@ -167,6 +170,8 @@ pub async fn omni_gen_image_generate_handler(
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
info!(">>> GENERATED!!");
|
||||
|
||||
let external_job_id = match &generation_response {
|
||||
artcraft_router::generate::generate_image::generate_image_response::GenerateImageResponse::Artcraft(p) => {
|
||||
p.inference_job_token.as_str().to_string()
|
||||
|
||||
+19
-26
@@ -54,11 +54,8 @@ pub struct DistilledVideoRequest {
|
||||
/// Cost estimate as computed by the Artcraft provider — this is what we bill on.
|
||||
pub cost: VideoGenerationCostEstimate,
|
||||
|
||||
/// Execution plan as computed by the execution provider — what we hand to the router.
|
||||
/// Execution plan as computed by the Fal provider — what we hand to the router.
|
||||
plan: VideoGenerationPlan<'static>,
|
||||
|
||||
/// The provider used for execution (Fal, Seedance2Pro, etc.).
|
||||
pub execution_provider: Provider,
|
||||
}
|
||||
|
||||
impl DistilledVideoRequest {
|
||||
@@ -79,7 +76,6 @@ impl DistilledVideoRequest {
|
||||
pub fn distill_video_request(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
media_file_hydration_map: Option<&HashMap<MediaFileToken, Url>>,
|
||||
execution_provider: Provider,
|
||||
) -> Result<DistilledVideoRequest, AdvancedCommonWebError> {
|
||||
// 1. Convert the raw API request into a router request.
|
||||
let initial = hydrate_to_router_request(request)?;
|
||||
@@ -113,8 +109,9 @@ pub fn distill_video_request(
|
||||
media_file_hydration_map,
|
||||
)?;
|
||||
|
||||
// 3. Build static lifetime references for text fields.
|
||||
// SAFETY: each box lives in `Self` for the full lifetime of `DistilledVideoRequest`.
|
||||
// 3. Build a `GenerateVideoRequest<'static>` whose borrows point into the
|
||||
// owned boxes. SAFETY: same invariant as the image version — each box
|
||||
// lives in `Self` for the full lifetime of `DistilledVideoRequest`.
|
||||
let prompt_static: Option<&'static str> = owned_prompt.as_deref().map(|s: &String| {
|
||||
let raw: *const str = s.as_str();
|
||||
unsafe { &*raw }
|
||||
@@ -130,23 +127,6 @@ pub fn distill_video_request(
|
||||
unsafe { &*raw }
|
||||
});
|
||||
|
||||
// 4. Cost estimate (Artcraft provider).
|
||||
// `initial` already has media fields in token form (from hydrate_to_router_request),
|
||||
// which is exactly what the Artcraft cost builder needs. Just swap the provider.
|
||||
let cost: VideoGenerationCostEstimate = {
|
||||
let cost_request = GenerateVideoRequest {
|
||||
provider: Provider::Artcraft,
|
||||
..initial
|
||||
};
|
||||
let cost_plan = cost_request.build().map_err(|e| {
|
||||
warn!("Failed to build cost plan during video distillation: {}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
cost_plan.estimate_costs()
|
||||
};
|
||||
|
||||
// 5. Execution plan (execution provider).
|
||||
// The execution plan uses URL-form media references (resolved from the hydration map).
|
||||
let start_frame_static: Option<ImageRef<'static>> =
|
||||
owned_start_frame_url.as_deref().map(|s: &String| {
|
||||
let raw: *const str = s.as_str();
|
||||
@@ -175,7 +155,7 @@ pub fn distill_video_request(
|
||||
|
||||
let request_static: GenerateVideoRequest<'static> = GenerateVideoRequest {
|
||||
model: initial.model,
|
||||
provider: execution_provider,
|
||||
provider: Provider::Fal,
|
||||
prompt: prompt_static,
|
||||
negative_prompt: negative_prompt_static,
|
||||
start_frame: start_frame_static,
|
||||
@@ -193,6 +173,20 @@ pub fn distill_video_request(
|
||||
idempotency_token: idempotency_static,
|
||||
};
|
||||
|
||||
// 4. Cost estimate (Artcraft provider).
|
||||
let cost_request = GenerateVideoRequest {
|
||||
provider: Provider::Artcraft,
|
||||
..request_static
|
||||
};
|
||||
let cost_plan = cost_request.build().map_err(|e| {
|
||||
warn!("Failed to build cost plan during video distillation: {}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
let cost: VideoGenerationCostEstimate = cost_plan.estimate_costs();
|
||||
drop(cost_plan);
|
||||
drop(cost_request);
|
||||
|
||||
// 5. Execution plan (Fal provider).
|
||||
let plan: VideoGenerationPlan<'static> = {
|
||||
let request_ref: &'static GenerateVideoRequest<'static> = unsafe {
|
||||
let raw: *const GenerateVideoRequest<'static> = &request_static;
|
||||
@@ -216,7 +210,6 @@ pub fn distill_video_request(
|
||||
request: request_static,
|
||||
cost,
|
||||
plan,
|
||||
execution_provider,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
-311
@@ -1,311 +0,0 @@
|
||||
//! Execution helpers for the omni-gen video generation handler.
|
||||
//!
|
||||
//! Routes generation to the appropriate provider (Fal or Seedance2Pro/Kinovi)
|
||||
//! based on the distilled request's `execution_provider`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use log::{info, warn};
|
||||
use url::Url;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{
|
||||
generate_video, BatchCount, GenerateVideoArgs, ModelType, Resolution,
|
||||
};
|
||||
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{
|
||||
prepare_file_upload, PrepareFileUploadArgs,
|
||||
};
|
||||
use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use url_utils::extension::extract_extension_from_url::{
|
||||
extract_extension_from_url, ExtractExtensions,
|
||||
};
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
|
||||
|
||||
use super::distill_video_request::DistilledVideoRequest;
|
||||
|
||||
/// Result of a successful generation, regardless of provider.
|
||||
pub struct GenerationResult {
|
||||
/// The external job ID used to track the generation (first order_id for Seedance).
|
||||
pub external_job_id: String,
|
||||
|
||||
/// Whether this is a Seedance2Pro generation (changes DB insertion path).
|
||||
pub is_seedance2pro: bool,
|
||||
|
||||
/// For Seedance2Pro batch jobs, the list of all order IDs.
|
||||
pub maybe_seedance_order_ids: Option<Vec<String>>,
|
||||
|
||||
/// The generation mode (Text, Keyframe, Reference).
|
||||
pub generation_mode: CommonGenerationMode,
|
||||
}
|
||||
|
||||
/// Execute video generation via the appropriate provider.
|
||||
pub async fn execute_generation(
|
||||
distilled: &DistilledVideoRequest,
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
server_state: &ServerState,
|
||||
media_file_hydration_map: Option<&HashMap<MediaFileToken, Url>>,
|
||||
) -> Result<GenerationResult, AdvancedCommonWebError> {
|
||||
match distilled.execution_provider {
|
||||
Provider::Seedance2Pro => {
|
||||
execute_generation_kinovi(
|
||||
distilled,
|
||||
request,
|
||||
server_state,
|
||||
media_file_hydration_map,
|
||||
).await
|
||||
}
|
||||
_ => {
|
||||
execute_generation_fal(
|
||||
distilled,
|
||||
request,
|
||||
server_state,
|
||||
).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute generation via Fal (the existing path).
|
||||
async fn execute_generation_fal(
|
||||
distilled: &DistilledVideoRequest,
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
server_state: &ServerState,
|
||||
) -> Result<GenerationResult, AdvancedCommonWebError> {
|
||||
let fal_client = artcraft_router::client::router_fal_client::RouterFalClient::new(
|
||||
server_state.fal.api_key.clone(),
|
||||
server_state.fal.webhook_url.clone(),
|
||||
);
|
||||
let router_client = artcraft_router::client::router_client::RouterClient::Fal(fal_client);
|
||||
|
||||
let generation_response = distilled.plan().generate_video(&router_client)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Video generation failed (Fal): {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
let external_job_id = match &generation_response {
|
||||
GenerateVideoResponse::Artcraft(p) => p.inference_job_token.as_str().to_string(),
|
||||
GenerateVideoResponse::Muapi(p) => p.request_id.as_str().to_string(),
|
||||
GenerateVideoResponse::Seedance2Pro(p) => p.order_id.clone(),
|
||||
GenerateVideoResponse::Fal(p) => p.request_id.clone().unwrap_or_default(),
|
||||
};
|
||||
|
||||
let generation_mode = if request.start_frame_image_media_token.is_some() {
|
||||
CommonGenerationMode::Keyframe
|
||||
} else {
|
||||
CommonGenerationMode::Text
|
||||
};
|
||||
|
||||
Ok(GenerationResult {
|
||||
external_job_id,
|
||||
is_seedance2pro: false,
|
||||
maybe_seedance_order_ids: None,
|
||||
generation_mode,
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute generation via Seedance2Pro/Kinovi.
|
||||
///
|
||||
/// Downloads all referenced media from our CDN, re-uploads to Seedance2Pro's
|
||||
/// CDN, then calls the Seedance2Pro generate endpoint.
|
||||
async fn execute_generation_kinovi(
|
||||
distilled: &DistilledVideoRequest,
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
server_state: &ServerState,
|
||||
media_file_hydration_map: Option<&HashMap<MediaFileToken, Url>>,
|
||||
) -> Result<GenerationResult, AdvancedCommonWebError> {
|
||||
let session = Seedance2ProSession::from_cookies_string(
|
||||
server_state.seedance2pro.cookies.clone()
|
||||
);
|
||||
|
||||
let empty_map = HashMap::new();
|
||||
let hydration_map = media_file_hydration_map.unwrap_or(&empty_map);
|
||||
|
||||
// Upload each media reference to Seedance2Pro CDN.
|
||||
let start_frame_url = upload_token_to_seedance2pro(
|
||||
&session, hydration_map, request.start_frame_image_media_token.as_ref(),
|
||||
).await?;
|
||||
|
||||
let end_frame_url = upload_token_to_seedance2pro(
|
||||
&session, hydration_map, request.end_frame_image_media_token.as_ref(),
|
||||
).await?;
|
||||
|
||||
let reference_image_urls = upload_tokens_to_seedance2pro(
|
||||
&session, hydration_map, request.reference_image_media_tokens.as_deref(),
|
||||
).await?;
|
||||
|
||||
let reference_video_urls = upload_tokens_to_seedance2pro(
|
||||
&session, hydration_map, request.reference_video_media_tokens.as_deref(),
|
||||
).await?;
|
||||
|
||||
let reference_audio_urls = upload_tokens_to_seedance2pro(
|
||||
&session, hydration_map, request.reference_audio_media_tokens.as_deref(),
|
||||
).await?;
|
||||
|
||||
// Determine generation mode.
|
||||
let is_keyframe = request.start_frame_image_media_token.is_some()
|
||||
|| request.end_frame_image_media_token.is_some();
|
||||
let is_reference = request.reference_image_media_tokens.as_ref().map_or(false, |t| !t.is_empty())
|
||||
|| request.reference_video_media_tokens.as_ref().map_or(false, |t| !t.is_empty())
|
||||
|| request.reference_audio_media_tokens.as_ref().map_or(false, |t| !t.is_empty());
|
||||
|
||||
let generation_mode = if is_keyframe {
|
||||
CommonGenerationMode::Keyframe
|
||||
} else if is_reference {
|
||||
CommonGenerationMode::Reference
|
||||
} else {
|
||||
CommonGenerationMode::Text
|
||||
};
|
||||
|
||||
// Map aspect ratio / duration / batch from the request.
|
||||
let resolution = Resolution::Landscape16x9; // TODO: Map from request.aspect_ratio
|
||||
let duration_seconds = request.duration_seconds.unwrap_or(5).clamp(4, 15) as u8;
|
||||
let batch_count = match request.video_batch_count {
|
||||
Some(2) => BatchCount::Two,
|
||||
Some(4) => BatchCount::Four,
|
||||
_ => BatchCount::One,
|
||||
};
|
||||
|
||||
let prompt = request.prompt.clone().unwrap_or_default();
|
||||
|
||||
let model_type = match request.model {
|
||||
Some(CommonVideoModel::Seedance2p0Fast) => ModelType::Seedance2Fast,
|
||||
_ => ModelType::Seedance2Pro,
|
||||
};
|
||||
|
||||
let video_gen_args = GenerateVideoArgs {
|
||||
session: &session,
|
||||
model_type,
|
||||
prompt,
|
||||
resolution,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
character_ids: None, // TODO: resolve_kinovi_character_ids when character support is added
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
|
||||
let gen_response = generate_video(video_gen_args).await
|
||||
.map_err(|err| {
|
||||
warn!("Seedance2Pro generate_video failed: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
info!(
|
||||
"Seedance2Pro generation: task_id={}, order_id={}",
|
||||
gen_response.task_id, gen_response.order_id
|
||||
);
|
||||
|
||||
let order_ids: Vec<String> = match gen_response.order_ids {
|
||||
Some(ids) if !ids.is_empty() => ids,
|
||||
_ => vec![gen_response.order_id.clone()],
|
||||
};
|
||||
|
||||
Ok(GenerationResult {
|
||||
external_job_id: gen_response.order_id,
|
||||
is_seedance2pro: true,
|
||||
maybe_seedance_order_ids: Some(order_ids),
|
||||
generation_mode,
|
||||
})
|
||||
}
|
||||
|
||||
// --- Upload helpers ---
|
||||
|
||||
/// Upload a single media token (if present) to Seedance2Pro CDN.
|
||||
async fn upload_token_to_seedance2pro(
|
||||
session: &Seedance2ProSession,
|
||||
hydration_map: &HashMap<MediaFileToken, Url>,
|
||||
maybe_token: Option<&MediaFileToken>,
|
||||
) -> Result<Option<String>, AdvancedCommonWebError> {
|
||||
let token = match maybe_token {
|
||||
None => return Ok(None),
|
||||
Some(t) => t,
|
||||
};
|
||||
|
||||
let url = hydration_map.get(token).ok_or_else(|| {
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
format!("Media token not found in hydration map: {:?}", token))
|
||||
})?;
|
||||
|
||||
let uploaded = upload_url_to_seedance2pro(session, url).await?;
|
||||
Ok(Some(uploaded))
|
||||
}
|
||||
|
||||
/// Upload a list of media tokens (if present) to Seedance2Pro CDN.
|
||||
async fn upload_tokens_to_seedance2pro(
|
||||
session: &Seedance2ProSession,
|
||||
hydration_map: &HashMap<MediaFileToken, Url>,
|
||||
maybe_tokens: Option<&[MediaFileToken]>,
|
||||
) -> Result<Option<Vec<String>>, AdvancedCommonWebError> {
|
||||
let tokens = match maybe_tokens {
|
||||
None => return Ok(None),
|
||||
Some(tokens) if tokens.is_empty() => return Ok(None),
|
||||
Some(tokens) => tokens,
|
||||
};
|
||||
|
||||
let mut urls = Vec::with_capacity(tokens.len());
|
||||
for token in tokens {
|
||||
let url = hydration_map.get(token).ok_or_else(|| {
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
format!("Media token not found in hydration map: {:?}", token))
|
||||
})?;
|
||||
urls.push(upload_url_to_seedance2pro(session, url).await?);
|
||||
}
|
||||
Ok(Some(urls))
|
||||
}
|
||||
|
||||
/// Download from our CDN and re-upload to Seedance2Pro.
|
||||
async fn upload_url_to_seedance2pro(
|
||||
session: &Seedance2ProSession,
|
||||
our_cdn_url: &Url,
|
||||
) -> Result<String, AdvancedCommonWebError> {
|
||||
let extension = extract_extension_from_url(our_cdn_url, &ExtractExtensions::All)
|
||||
.map(|ext| ext.without_period().to_string())
|
||||
.unwrap_or_else(|| "png".to_string());
|
||||
|
||||
let file_bytes = http_download_url_to_bytes(our_cdn_url.as_str())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error downloading media from CDN for Seedance2Pro upload: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?
|
||||
.to_vec();
|
||||
|
||||
let prepare_result = prepare_file_upload(PrepareFileUploadArgs {
|
||||
session,
|
||||
extension,
|
||||
host_override: None,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error preparing seedance2pro file upload: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let upload_result = upload_file(UploadFileArgs {
|
||||
upload_url: prepare_result.upload_url,
|
||||
file_bytes,
|
||||
host_override: None,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error uploading file to seedance2pro: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
Ok(upload_result.public_url)
|
||||
}
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
pub mod omni_gen_video_generate_handler;
|
||||
pub mod distill_video_request;
|
||||
pub(crate) mod distill_helper;
|
||||
mod execute_generation;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
+62
-103
@@ -9,10 +9,9 @@ use url::Url;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::generate_response::omni_gen_video_generate_response::OmniGenVideoGenerateResponse;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::visibility::Visibility;
|
||||
@@ -22,10 +21,6 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
|
||||
insert_generic_inference_job_for_fal_queue_with_apriori_job_token,
|
||||
InsertGenericInferenceForFalWithAprioriJobTokenArgs,
|
||||
};
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token::{
|
||||
insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token,
|
||||
InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs,
|
||||
};
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{
|
||||
insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem,
|
||||
@@ -42,7 +37,6 @@ use crate::state::server_state::ServerState;
|
||||
use crate::util::lookup::lookup_image_urls_as_map::lookup_image_urls_as_map;
|
||||
|
||||
use super::distill_video_request::distill_video_request;
|
||||
use super::execute_generation::execute_generation;
|
||||
|
||||
/// Generate a video using the omni-gen unified endpoint.
|
||||
#[utoipa::path(
|
||||
@@ -155,17 +149,12 @@ pub async fn omni_gen_video_generate_handler(
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== DETERMINE PROVIDER ==================== //
|
||||
|
||||
let execution_provider = match request.model {
|
||||
Some(CommonVideoModel::Seedance2p0) => Provider::Seedance2Pro,
|
||||
Some(CommonVideoModel::Seedance2p0Fast) => Provider::Seedance2Pro,
|
||||
_ => Provider::Fal,
|
||||
};
|
||||
|
||||
// ==================== DISTILL ==================== //
|
||||
|
||||
let distilled = distill_video_request(&request, media_file_hydration_map.as_ref(), execution_provider)?;
|
||||
let distilled = distill_video_request(&request, media_file_hydration_map.as_ref())?;
|
||||
|
||||
info!(">>> Distilled cost: {:?}", distilled.cost);
|
||||
info!(">>> Distilled plan: {:?}", distilled.plan());
|
||||
|
||||
// ==================== BILLING ==================== //
|
||||
|
||||
@@ -186,12 +175,33 @@ pub async fn omni_gen_video_generate_handler(
|
||||
|
||||
// ==================== EXECUTE GENERATION ==================== //
|
||||
|
||||
let gen_result = execute_generation(
|
||||
&distilled,
|
||||
&request,
|
||||
&server_state,
|
||||
media_file_hydration_map.as_ref(),
|
||||
).await?;
|
||||
let fal_client = artcraft_router::client::router_fal_client::RouterFalClient::new(
|
||||
server_state.fal.api_key.clone(),
|
||||
server_state.fal.webhook_url.clone(),
|
||||
);
|
||||
let router_client = artcraft_router::client::router_client::RouterClient::Fal(fal_client);
|
||||
|
||||
let generation_response = distilled.plan().generate_video(&router_client)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Video generation failed: {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
let external_job_id = match &generation_response {
|
||||
artcraft_router::generate::generate_video::generate_video_response::GenerateVideoResponse::Artcraft(p) => {
|
||||
p.inference_job_token.as_str().to_string()
|
||||
}
|
||||
artcraft_router::generate::generate_video::generate_video_response::GenerateVideoResponse::Muapi(p) => {
|
||||
p.request_id.as_str().to_string()
|
||||
}
|
||||
artcraft_router::generate::generate_video::generate_video_response::GenerateVideoResponse::Seedance2Pro(p) => {
|
||||
p.order_id.clone()
|
||||
}
|
||||
artcraft_router::generate::generate_video::generate_video_response::GenerateVideoResponse::Fal(p) => {
|
||||
p.request_id.clone().unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== DB TRANSACTION ==================== //
|
||||
|
||||
@@ -207,6 +217,12 @@ pub async fn omni_gen_video_generate_handler(
|
||||
|
||||
// -- Prompt --
|
||||
|
||||
let generation_mode = if request.start_frame_image_media_token.is_some() {
|
||||
CommonGenerationMode::Keyframe
|
||||
} else {
|
||||
CommonGenerationMode::Text
|
||||
};
|
||||
|
||||
let prompt_result = insert_prompt(InsertPromptArgs {
|
||||
maybe_apriori_prompt_token: None,
|
||||
prompt_type: PromptType::ArtcraftApp,
|
||||
@@ -216,7 +232,7 @@ pub async fn omni_gen_video_generate_handler(
|
||||
maybe_positive_prompt: request.prompt.as_deref(),
|
||||
maybe_negative_prompt: request.negative_prompt.as_deref(),
|
||||
maybe_other_args: None,
|
||||
maybe_generation_mode: Some(gen_result.generation_mode),
|
||||
maybe_generation_mode: Some(generation_mode),
|
||||
maybe_aspect_ratio: None,
|
||||
maybe_resolution: None,
|
||||
maybe_batch_count: request.video_batch_count.map(|c| c as u8),
|
||||
@@ -285,88 +301,31 @@ pub async fn omni_gen_video_generate_handler(
|
||||
|
||||
// -- Inference job --
|
||||
|
||||
let job_token = if gen_result.is_seedance2pro {
|
||||
// Seedance2Pro path: insert one job per order_id (batch support).
|
||||
let fallback_ids = vec![gen_result.external_job_id.clone()];
|
||||
let order_ids = gen_result.maybe_seedance_order_ids
|
||||
.as_deref()
|
||||
.unwrap_or(&fallback_ids);
|
||||
|
||||
let mut all_job_tokens: Vec<InferenceJobToken> = Vec::with_capacity(order_ids.len());
|
||||
|
||||
for (i, order_id) in order_ids.iter().enumerate() {
|
||||
let job_token = if i == 0 {
|
||||
apriori_job_token.clone()
|
||||
} else {
|
||||
InferenceJobToken::generate()
|
||||
};
|
||||
|
||||
let idempotency_str = if i == 0 {
|
||||
idempotency_token.clone()
|
||||
} else {
|
||||
format!("{}-batch-{}", idempotency_token, i)
|
||||
};
|
||||
|
||||
let db_result = insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token(
|
||||
InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs {
|
||||
apriori_job_token: &job_token,
|
||||
uuid_idempotency_token: &idempotency_str,
|
||||
maybe_external_third_party_id: order_id,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: prompt_token.as_ref(),
|
||||
maybe_wallet_ledger_entry_token: None,
|
||||
maybe_creator_user_token: Some(user_token),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}
|
||||
).await;
|
||||
|
||||
match db_result {
|
||||
Ok(token) => all_job_tokens.push(token),
|
||||
Err(err) => {
|
||||
warn!("Error inserting seedance2pro inference job (order_id={}): {:?}", order_id, err);
|
||||
if i == 0 {
|
||||
return Err(AdvancedCommonWebError::from_error(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
let db_result = insert_generic_inference_job_for_fal_queue_with_apriori_job_token(
|
||||
InsertGenericInferenceForFalWithAprioriJobTokenArgs {
|
||||
apriori_job_token: &apriori_job_token,
|
||||
uuid_idempotency_token: &idempotency_token,
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::VideoGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: prompt_token.as_ref(),
|
||||
maybe_creator_user_token: Some(user_token),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_executor: &mut *transaction,
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
phantom: Default::default(),
|
||||
}
|
||||
).await;
|
||||
|
||||
all_job_tokens.first().cloned().ok_or_else(|| {
|
||||
error!("No inference job token was created");
|
||||
AdvancedCommonWebError::server_error_with_message("No inference job token was created")
|
||||
})?
|
||||
} else {
|
||||
// Fal / other providers path.
|
||||
let db_result = insert_generic_inference_job_for_fal_queue_with_apriori_job_token(
|
||||
InsertGenericInferenceForFalWithAprioriJobTokenArgs {
|
||||
apriori_job_token: &apriori_job_token,
|
||||
uuid_idempotency_token: &idempotency_token,
|
||||
maybe_external_third_party_id: &gen_result.external_job_id,
|
||||
fal_category: FalCategory::VideoGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: prompt_token.as_ref(),
|
||||
maybe_creator_user_token: Some(user_token),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_executor: &mut *transaction,
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
phantom: Default::default(),
|
||||
}
|
||||
).await;
|
||||
|
||||
match db_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting inference job: {:?}", err);
|
||||
return Err(AdvancedCommonWebError::from_error(err));
|
||||
}
|
||||
let job_token = match db_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting inference job: {:?}", err);
|
||||
return Err(AdvancedCommonWebError::from_error(err));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
mod test_kinovi_seedance_2p0;
|
||||
-459
@@ -1,459 +0,0 @@
|
||||
//! Tests for omni-gen video against the Seedance 2.0 model (Kinovi/Seedance2Pro provider).
|
||||
//!
|
||||
//! Seedance 2.0 is special: it uses the Seedance2Pro/Kinovi provider for execution
|
||||
//! instead of Fal. The cost estimation uses Artcraft provider with media-token-form
|
||||
//! references, while the execution plan uses URL-form references.
|
||||
//!
|
||||
//! Pricing: 16¢ per second × batch count.
|
||||
//! - 4s/batch 1 = 64¢, 5s/batch 1 = 80¢, 15s/batch 1 = 240¢
|
||||
//! - Batch 2 = 2×, Batch 4 = 4×
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use url::Url;
|
||||
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::distill_video_request::{
|
||||
distill_video_request, DistilledVideoRequest,
|
||||
};
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_request(
|
||||
prompt: Option<&str>,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
duration_seconds: Option<u16>,
|
||||
video_batch_count: Option<u16>,
|
||||
start_frame_token: Option<MediaFileToken>,
|
||||
end_frame_token: Option<MediaFileToken>,
|
||||
reference_image_tokens: Option<Vec<MediaFileToken>>,
|
||||
reference_video_tokens: Option<Vec<MediaFileToken>>,
|
||||
reference_audio_tokens: Option<Vec<MediaFileToken>>,
|
||||
) -> OmniGenVideoCostAndGenerateRequest {
|
||||
OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some("11111111-1111-1111-1111-111111111111".to_string()),
|
||||
model: Some(CommonVideoModel::Seedance2p0),
|
||||
prompt: prompt.map(|s| s.to_string()),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: start_frame_token,
|
||||
end_frame_image_media_token: end_frame_token,
|
||||
reference_image_media_tokens: reference_image_tokens,
|
||||
reference_video_media_tokens: reference_video_tokens,
|
||||
reference_audio_media_tokens: reference_audio_tokens,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio,
|
||||
quality: None,
|
||||
duration_seconds,
|
||||
video_batch_count,
|
||||
generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn text_only_request(
|
||||
duration_seconds: Option<u16>,
|
||||
video_batch_count: Option<u16>,
|
||||
) -> OmniGenVideoCostAndGenerateRequest {
|
||||
make_request(
|
||||
Some("a cat dancing"),
|
||||
None,
|
||||
duration_seconds,
|
||||
video_batch_count,
|
||||
None, None, None, None, None,
|
||||
)
|
||||
}
|
||||
|
||||
fn fake_token(name: &str) -> MediaFileToken {
|
||||
MediaFileToken::new_from_str(name)
|
||||
}
|
||||
|
||||
fn build_hydration_map(tokens: &[&MediaFileToken]) -> HashMap<MediaFileToken, Url> {
|
||||
tokens.iter().enumerate().map(|(i, t)| {
|
||||
let url = Url::parse(&format!("https://cdn.example.com/media/{}.png", i)).unwrap();
|
||||
((*t).clone(), url)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn distill_text(request: &OmniGenVideoCostAndGenerateRequest) -> DistilledVideoRequest {
|
||||
let empty: HashMap<MediaFileToken, Url> = HashMap::new();
|
||||
distill_video_request(request, Some(&empty), Provider::Seedance2Pro)
|
||||
.expect("distill should succeed for Seedance 2.0 (text)")
|
||||
}
|
||||
|
||||
fn distill_with_map(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
hydration: &HashMap<MediaFileToken, Url>,
|
||||
) -> DistilledVideoRequest {
|
||||
distill_video_request(request, Some(hydration), Provider::Seedance2Pro)
|
||||
.expect("distill should succeed for Seedance 2.0 (with media)")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COST TESTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
mod cost_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_is_present() {
|
||||
let d = distill_text(&text_only_request(Some(5), None));
|
||||
assert!(d.cost.cost_in_credits.is_some());
|
||||
assert!(d.cost.cost_in_usd_cents.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_is_not_free() {
|
||||
let d = distill_text(&text_only_request(Some(5), None));
|
||||
assert!(!d.cost.is_free);
|
||||
}
|
||||
|
||||
// Pricing: 16¢/sec × batch_count
|
||||
|
||||
#[test]
|
||||
fn cost_4s_batch_1() {
|
||||
let d = distill_text(&text_only_request(Some(4), Some(1)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(64));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_5s_batch_1() {
|
||||
let d = distill_text(&text_only_request(Some(5), Some(1)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(80));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_6s_batch_1() {
|
||||
let d = distill_text(&text_only_request(Some(6), Some(1)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(96));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_10s_batch_1() {
|
||||
let d = distill_text(&text_only_request(Some(10), Some(1)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(160));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_15s_batch_1() {
|
||||
let d = distill_text(&text_only_request(Some(15), Some(1)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(240));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_5s_batch_2() {
|
||||
let d = distill_text(&text_only_request(Some(5), Some(2)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(160));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_5s_batch_4() {
|
||||
let d = distill_text(&text_only_request(Some(5), Some(4)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(320));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_15s_batch_4() {
|
||||
let d = distill_text(&text_only_request(Some(15), Some(4)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(960));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_same_with_or_without_start_frame() {
|
||||
// Cost should not change based on media presence — it's duration × batch.
|
||||
let text_d = distill_text(&text_only_request(Some(5), Some(1)));
|
||||
|
||||
let start_token = fake_token("start_frame");
|
||||
let map = build_hydration_map(&[&start_token]);
|
||||
let req = make_request(
|
||||
Some("a cat"), None, Some(5), Some(1),
|
||||
Some(start_token), None, None, None, None,
|
||||
);
|
||||
let frame_d = distill_with_map(&req, &map);
|
||||
|
||||
assert_eq!(text_d.cost.cost_in_usd_cents, frame_d.cost.cost_in_usd_cents);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_estimation_uses_media_tokens_not_urls() {
|
||||
// The Artcraft cost plan should use token-form references.
|
||||
// If it used URLs, the Artcraft plan builder would reject them.
|
||||
// This test passes if distillation succeeds at all — the Artcraft
|
||||
// builder would error out on URL-form refs.
|
||||
let start_token = fake_token("start_frame");
|
||||
let ref_img = fake_token("ref_img_1");
|
||||
let ref_vid = fake_token("ref_vid_1");
|
||||
let ref_aud = fake_token("ref_aud_1");
|
||||
|
||||
let map = build_hydration_map(&[&start_token, &ref_img, &ref_vid, &ref_aud]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), Some(1),
|
||||
Some(start_token), None,
|
||||
Some(vec![ref_img]),
|
||||
Some(vec![ref_vid]),
|
||||
Some(vec![ref_aud]),
|
||||
);
|
||||
|
||||
// If this succeeds, the Artcraft cost path handled tokens correctly.
|
||||
let d = distill_with_map(&req, &map);
|
||||
assert!(d.cost.cost_in_credits.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_duration_produces_valid_cost() {
|
||||
// When duration_seconds is None, the model should use a default.
|
||||
let d = distill_text(&text_only_request(None, None));
|
||||
assert!(d.cost.cost_in_usd_cents.unwrap_or(0) > 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// GENERATION PLAN TESTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
mod generation_plan_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn provider_is_seedance2pro() {
|
||||
let d = distill_text(&text_only_request(Some(5), None));
|
||||
assert!(
|
||||
matches!(d.execution_provider, Provider::Seedance2Pro),
|
||||
"expected Seedance2Pro provider, got {:?}", d.execution_provider
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_is_seedance2pro_variant() {
|
||||
let d = distill_text(&text_only_request(Some(5), None));
|
||||
assert!(
|
||||
matches!(d.plan(), VideoGenerationPlan::Seedance2proSeedance2p0(_)),
|
||||
"expected Seedance2proSeedance2p0 plan variant, got {:?}", d.plan()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_only_has_no_media_urls() {
|
||||
let d = distill_text(&text_only_request(Some(5), None));
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
assert!(plan.start_frame_url.is_none());
|
||||
assert!(plan.end_frame_url.is_none());
|
||||
assert!(plan.reference_image_urls.is_none());
|
||||
assert!(plan.reference_video_urls.is_none());
|
||||
assert!(plan.reference_audio_urls.is_none());
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_only_prompt_is_passed_through() {
|
||||
let d = distill_text(&text_only_request(Some(5), None));
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
assert_eq!(plan.prompt.as_deref(), Some("a cat dancing"));
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_frame_uses_url_not_token() {
|
||||
let start_token = fake_token("start_frame");
|
||||
let map = build_hydration_map(&[&start_token]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), None,
|
||||
Some(start_token), None, None, None, None,
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
let url = plan.start_frame_url.as_ref().expect("start_frame_url should be set");
|
||||
assert!(url.starts_with("https://"), "expected URL, got: {}", url);
|
||||
assert!(!url.contains("m_"), "URL should not look like a media token");
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_frame_uses_url_not_token() {
|
||||
let end_token = fake_token("end_frame");
|
||||
let map = build_hydration_map(&[&end_token]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), None,
|
||||
None, Some(end_token), None, None, None,
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
let url = plan.end_frame_url.as_ref().expect("end_frame_url should be set");
|
||||
assert!(url.starts_with("https://"), "expected URL, got: {}", url);
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_and_end_frame_both_resolved() {
|
||||
let start = fake_token("sf");
|
||||
let end = fake_token("ef");
|
||||
let map = build_hydration_map(&[&start, &end]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), None,
|
||||
Some(start), Some(end), None, None, None,
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
assert!(plan.start_frame_url.is_some());
|
||||
assert!(plan.end_frame_url.is_some());
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_images_use_urls() {
|
||||
let img1 = fake_token("img1");
|
||||
let img2 = fake_token("img2");
|
||||
let map = build_hydration_map(&[&img1, &img2]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), None,
|
||||
None, None,
|
||||
Some(vec![img1, img2]),
|
||||
None, None,
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
let urls = plan.reference_image_urls.as_ref().expect("reference_image_urls should be set");
|
||||
assert_eq!(urls.len(), 2);
|
||||
for url in urls {
|
||||
assert!(url.starts_with("https://"), "expected URL, got: {}", url);
|
||||
}
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_videos_use_urls() {
|
||||
let vid1 = fake_token("vid1");
|
||||
let map = build_hydration_map(&[&vid1]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), None,
|
||||
None, None, None,
|
||||
Some(vec![vid1]),
|
||||
None,
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
let urls = plan.reference_video_urls.as_ref().expect("reference_video_urls should be set");
|
||||
assert_eq!(urls.len(), 1);
|
||||
assert!(urls[0].starts_with("https://"));
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_audio_use_urls() {
|
||||
let aud1 = fake_token("aud1");
|
||||
let map = build_hydration_map(&[&aud1]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), None,
|
||||
None, None, None, None,
|
||||
Some(vec![aud1]),
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
let urls = plan.reference_audio_urls.as_ref().expect("reference_audio_urls should be set");
|
||||
assert_eq!(urls.len(), 1);
|
||||
assert!(urls[0].starts_with("https://"));
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_reference_types_together() {
|
||||
let sf = fake_token("sf");
|
||||
let ef = fake_token("ef");
|
||||
let img = fake_token("img");
|
||||
let vid = fake_token("vid");
|
||||
let aud = fake_token("aud");
|
||||
let map = build_hydration_map(&[&sf, &ef, &img, &vid, &aud]);
|
||||
|
||||
let req = make_request(
|
||||
Some("full test"), None, Some(8), Some(2),
|
||||
Some(sf), Some(ef),
|
||||
Some(vec![img]),
|
||||
Some(vec![vid]),
|
||||
Some(vec![aud]),
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
assert!(plan.start_frame_url.is_some());
|
||||
assert!(plan.end_frame_url.is_some());
|
||||
assert_eq!(plan.reference_image_urls.as_ref().map(|v| v.len()), Some(1));
|
||||
assert_eq!(plan.reference_video_urls.as_ref().map(|v| v.len()), Some(1));
|
||||
assert_eq!(plan.reference_audio_urls.as_ref().map(|v| v.len()), Some(1));
|
||||
assert!(plan.prompt.as_deref() == Some("full test"));
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_is_clamped_to_valid_range() {
|
||||
// Duration below 4 should clamp to 4.
|
||||
let d = distill_text(&text_only_request(Some(1), None));
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
assert!(plan.duration_seconds >= 4, "expected >= 4, got {}", plan.duration_seconds);
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
|
||||
// Duration above 15 should clamp to 15.
|
||||
let d = distill_text(&text_only_request(Some(99), None));
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
assert!(plan.duration_seconds <= 15, "expected <= 15, got {}", plan.duration_seconds);
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prompt_defaults_to_empty() {
|
||||
let req = make_request(
|
||||
None, None, Some(5), None,
|
||||
None, None, None, None, None,
|
||||
);
|
||||
let d = distill_text(&req);
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
// Prompt should be None or empty string.
|
||||
let prompt = plan.prompt.as_deref().unwrap_or("");
|
||||
assert!(prompt.is_empty() || plan.prompt.is_none());
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-3
@@ -1,4 +1 @@
|
||||
mod test_seedance_1p5_pro;
|
||||
mod test_veo_2;
|
||||
mod test_veo_3;
|
||||
mod kinovi;
|
||||
|
||||
+3
-4
@@ -1,4 +1,4 @@
|
||||
//! Tests for omni-gen video against the Seedance 1.5 Pro model.
|
||||
//! Tests for omni-gen video distillation against the Seedance 1.5 Pro model.
|
||||
//!
|
||||
//! Seedance 1.5 Pro supports both text-to-video and image-to-video (via
|
||||
//! start_frame / end_frame). Pricing is based on resolution × duration ×
|
||||
@@ -19,7 +19,6 @@ mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use url::Url;
|
||||
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::distill_video_request::{
|
||||
distill_video_request, DistilledVideoRequest,
|
||||
};
|
||||
@@ -76,7 +75,7 @@ mod tests {
|
||||
|
||||
fn distill_text(request: &OmniGenVideoCostAndGenerateRequest) -> DistilledVideoRequest {
|
||||
let empty: HashMap<MediaFileToken, Url> = HashMap::new();
|
||||
distill_video_request(request, Some(&empty), Provider::Fal)
|
||||
distill_video_request(request, Some(&empty))
|
||||
.expect("distill_video_request should succeed for Seedance 1.5 Pro (text)")
|
||||
}
|
||||
|
||||
@@ -84,7 +83,7 @@ mod tests {
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
hydration: &HashMap<MediaFileToken, Url>,
|
||||
) -> DistilledVideoRequest {
|
||||
distill_video_request(request, Some(hydration), Provider::Fal)
|
||||
distill_video_request(request, Some(hydration))
|
||||
.expect("distill_video_request should succeed for Seedance 1.5 Pro (image)")
|
||||
}
|
||||
|
||||
|
||||
-443
@@ -1,443 +0,0 @@
|
||||
//! Tests for omni-gen video against the Veo 2 model.
|
||||
//!
|
||||
//! Veo 2 supports both text-to-video and image-to-video (via start_frame).
|
||||
//!
|
||||
//! Pricing: $2.50 for the first 5 seconds, +$0.50 per additional second.
|
||||
//! 5s → 250¢, 6s → 300¢, 7s → 350¢, 8s → 400¢
|
||||
//!
|
||||
//! Supported aspect ratios: Auto, WideSixteenNine, TallNineSixteen.
|
||||
//! Unsupported ratios fall back to Auto under PayMoreUpgrade.
|
||||
//!
|
||||
//! Supported durations: 5–8 seconds. Over-max clamps to 8s (PayMoreUpgrade).
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use url::Url;
|
||||
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::distill_video_request::{
|
||||
distill_video_request, DistilledVideoRequest,
|
||||
};
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_request(
|
||||
prompt: Option<&str>,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
duration_seconds: Option<u16>,
|
||||
start_frame_token: Option<MediaFileToken>,
|
||||
) -> OmniGenVideoCostAndGenerateRequest {
|
||||
OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some("11111111-1111-1111-1111-111111111111".to_string()),
|
||||
model: Some(CommonVideoModel::Veo2),
|
||||
prompt: prompt.map(|s| s.to_string()),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: start_frame_token,
|
||||
end_frame_image_media_token: None,
|
||||
reference_image_media_tokens: None,
|
||||
reference_video_media_tokens: None,
|
||||
reference_audio_media_tokens: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio,
|
||||
quality: None,
|
||||
duration_seconds,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn fake_token(name: &str) -> (MediaFileToken, HashMap<MediaFileToken, Url>) {
|
||||
let token = MediaFileToken::new_from_str(name);
|
||||
let url = Url::parse(&format!("https://fake.example.com/{}.png", name)).unwrap();
|
||||
let mut map = HashMap::new();
|
||||
map.insert(token.clone(), url);
|
||||
(token, map)
|
||||
}
|
||||
|
||||
fn distill_text(request: &OmniGenVideoCostAndGenerateRequest) -> DistilledVideoRequest {
|
||||
let empty: HashMap<MediaFileToken, Url> = HashMap::new();
|
||||
distill_video_request(request, Some(&empty), Provider::Fal)
|
||||
.expect("distill_video_request should succeed for Veo 2 (text)")
|
||||
}
|
||||
|
||||
fn distill_image(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
hydration: &HashMap<MediaFileToken, Url>,
|
||||
) -> DistilledVideoRequest {
|
||||
distill_video_request(request, Some(hydration), Provider::Fal)
|
||||
.expect("distill_video_request should succeed for Veo 2 (image)")
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// COST
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
mod cost {
|
||||
use super::*;
|
||||
|
||||
mod text {
|
||||
use super::*;
|
||||
|
||||
fn cost(duration: Option<u16>) -> u64 {
|
||||
let request = make_request(Some("p"), None, duration, None);
|
||||
distill_text(&request).cost.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_duration_costs_250() { assert_eq!(cost(None), 250); }
|
||||
|
||||
#[test]
|
||||
fn five_seconds_costs_250() { assert_eq!(cost(Some(5)), 250); }
|
||||
|
||||
#[test]
|
||||
fn six_seconds_costs_300() { assert_eq!(cost(Some(6)), 300); }
|
||||
|
||||
#[test]
|
||||
fn seven_seconds_costs_350() { assert_eq!(cost(Some(7)), 350); }
|
||||
|
||||
#[test]
|
||||
fn eight_seconds_costs_400() { assert_eq!(cost(Some(8)), 400); }
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_to_400() { assert_eq!(cost(Some(15)), 400); }
|
||||
|
||||
#[test]
|
||||
fn cost_independent_of_aspect_ratio() {
|
||||
let ars = [
|
||||
None,
|
||||
Some(CommonAspectRatio::Auto),
|
||||
Some(CommonAspectRatio::WideSixteenByNine),
|
||||
Some(CommonAspectRatio::TallNineBySixteen),
|
||||
Some(CommonAspectRatio::Square),
|
||||
];
|
||||
for ar in ars {
|
||||
let request = make_request(Some("p"), ar, None, None);
|
||||
assert_eq!(distill_text(&request).cost.cost_in_usd_cents.unwrap(), 250);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_metadata_flags_are_default() {
|
||||
let d = distill_text(&make_request(Some("p"), None, None, None));
|
||||
assert!(!d.cost.is_free);
|
||||
assert!(!d.cost.is_unlimited);
|
||||
assert!(!d.cost.is_rate_limited);
|
||||
assert!(!d.cost.has_watermark);
|
||||
}
|
||||
}
|
||||
|
||||
mod image {
|
||||
use super::*;
|
||||
|
||||
fn cost(duration: Option<u16>) -> u64 {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, duration, Some(start));
|
||||
distill_image(&request, &map).cost.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_duration_costs_250() { assert_eq!(cost(None), 250); }
|
||||
|
||||
#[test]
|
||||
fn five_seconds_costs_250() { assert_eq!(cost(Some(5)), 250); }
|
||||
|
||||
#[test]
|
||||
fn six_seconds_costs_300() { assert_eq!(cost(Some(6)), 300); }
|
||||
|
||||
#[test]
|
||||
fn seven_seconds_costs_350() { assert_eq!(cost(Some(7)), 350); }
|
||||
|
||||
#[test]
|
||||
fn eight_seconds_costs_400() { assert_eq!(cost(Some(8)), 400); }
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_to_400() { assert_eq!(cost(Some(15)), 400); }
|
||||
|
||||
#[test]
|
||||
fn image_and_text_cost_match() {
|
||||
for dur in [5u16, 6, 7, 8] {
|
||||
let text = distill_text(&make_request(Some("p"), None, Some(dur), None))
|
||||
.cost.cost_in_usd_cents.unwrap();
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let img = distill_image(
|
||||
&make_request(Some("p"), None, Some(dur), Some(start)),
|
||||
&map,
|
||||
).cost.cost_in_usd_cents.unwrap();
|
||||
assert_eq!(text, img, "text/image cost diverged at dur={}", dur);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// PLAN
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
mod plan {
|
||||
use super::*;
|
||||
|
||||
use artcraft_router::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_router::generate::generate_video::plan::fal::plan_generate_video_fal_veo_2::{
|
||||
FalVeo2Mode, PlanFalVeo2,
|
||||
};
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{
|
||||
Veo2AspectRatio, Veo2Duration,
|
||||
};
|
||||
|
||||
fn with_text_plan<F: FnOnce(&PlanFalVeo2)>(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
assertion: F,
|
||||
) {
|
||||
let distilled = distill_text(request);
|
||||
match distilled.plan() {
|
||||
VideoGenerationPlan::FalVeo2(plan) => assertion(plan),
|
||||
other => panic!("expected FalVeo2, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_image_plan<F: FnOnce(&PlanFalVeo2)>(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
hydration: &HashMap<MediaFileToken, Url>,
|
||||
assertion: F,
|
||||
) {
|
||||
let distilled = distill_image(request, hydration);
|
||||
match distilled.plan() {
|
||||
VideoGenerationPlan::FalVeo2(plan) => assertion(plan),
|
||||
other => panic!("expected FalVeo2, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
mod text {
|
||||
use super::*;
|
||||
|
||||
// ── Mode detection ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn text_mode_is_text_to_video() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None), |plan| {
|
||||
assert!(matches!(plan.mode, FalVeo2Mode::TextToVideo));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Prompt passthrough ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
with_text_plan(&make_request(Some("a horse on mars"), None, None, None), |plan| {
|
||||
assert_eq!(plan.prompt, "a horse on mars");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prompt_is_empty_string() {
|
||||
with_text_plan(&make_request(None, None, None, None), |plan| {
|
||||
assert_eq!(plan.prompt, "");
|
||||
});
|
||||
}
|
||||
|
||||
// ── Aspect ratio mappings ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_aspect_ratio_is_auto() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None), |plan| {
|
||||
assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_yields_auto() {
|
||||
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None), |plan| {
|
||||
assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_16x9_yields_wide() {
|
||||
for ar in [CommonAspectRatio::WideSixteenByNine, CommonAspectRatio::Wide] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None), |plan| {
|
||||
assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::WideSixteenNine)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tall_9x16_yields_tall() {
|
||||
for ar in [CommonAspectRatio::TallNineBySixteen, CommonAspectRatio::Tall] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None), |plan| {
|
||||
assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::TallNineSixteen)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_aspect_ratio_falls_back_to_auto() {
|
||||
for ar in [
|
||||
CommonAspectRatio::Square, CommonAspectRatio::SquareHd,
|
||||
CommonAspectRatio::WideFourByThree, CommonAspectRatio::TallThreeByFour,
|
||||
] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None), |plan| {
|
||||
assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Duration mappings ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_duration_is_default() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None), |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::Default));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_5_yields_five() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(5), None), |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::FiveSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_6_yields_six() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(6), None), |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::SixSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_7_yields_seven() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(7), None), |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::SevenSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_8_yields_eight() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(8), None), |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_to_eight() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(20), None), |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mod image {
|
||||
use super::*;
|
||||
|
||||
// ── Mode detection / start frame hydration ────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn start_frame_triggers_image_to_video() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
match &plan.mode {
|
||||
FalVeo2Mode::ImageToVideo { image_url } => {
|
||||
assert!(image_url.starts_with("https://fake.example.com/"));
|
||||
}
|
||||
other => panic!("expected ImageToVideo, got {:?}", other),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_frame_url_matches_hydration_map_exactly() {
|
||||
let token = MediaFileToken::new_from_str("mf_myframe0000000000000000000000");
|
||||
let expected_url = "https://cdn.example.com/frames/my_frame.png";
|
||||
let mut map = HashMap::new();
|
||||
map.insert(token.clone(), Url::parse(expected_url).unwrap());
|
||||
|
||||
let request = make_request(Some("p"), None, None, Some(token));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
match &plan.mode {
|
||||
FalVeo2Mode::ImageToVideo { image_url } => {
|
||||
assert_eq!(image_url, expected_url);
|
||||
}
|
||||
other => panic!("expected ImageToVideo, got {:?}", other),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Prompt passthrough in image mode ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("a horse on mars"), None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert_eq!(plan.prompt, "a horse on mars");
|
||||
});
|
||||
}
|
||||
|
||||
// ── Aspect ratio is None in image mode (i2v inherits source AR) ────────
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_is_none_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(CommonAspectRatio::WideSixteenByNine), None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(plan.aspect_ratio.is_none(), "image-to-video should not have aspect_ratio");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_is_none_in_image_mode_even_with_tall() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(CommonAspectRatio::TallNineBySixteen), None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(plan.aspect_ratio.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
// ── Duration pass-throughs in image mode ──────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn duration_5_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, Some(5), Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::FiveSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_8_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, Some(8), Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, Some(20), Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_duration_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::Default));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-590
@@ -1,590 +0,0 @@
|
||||
//! Tests for omni-gen video against the Veo 3 model.
|
||||
//!
|
||||
//! Veo 3 supports both text-to-video and image-to-video (via start_frame).
|
||||
//!
|
||||
//! Pricing: $0.20/sec (audio off) or $0.40/sec (audio on).
|
||||
//! Default: 8s, audio off (artcraft legacy) → 160¢.
|
||||
//! 4s no-audio → 80¢, 6s no-audio → 120¢, 8s no-audio → 160¢
|
||||
//! 4s audio → 160¢, 6s audio → 240¢, 8s audio → 320¢
|
||||
//!
|
||||
//! Supported aspect ratios (text mode only): Default, 16:9, 9:16, Square.
|
||||
//! Image-to-video inherits the source frame's aspect ratio.
|
||||
//!
|
||||
//! Supported resolutions: 720p, 1080p.
|
||||
//! Supported durations: 4s, 6s, 8s. Over-max clamps to 8s (PayMoreUpgrade).
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use url::Url;
|
||||
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::distill_video_request::{
|
||||
distill_video_request, DistilledVideoRequest,
|
||||
};
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_request(
|
||||
prompt: Option<&str>,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: Option<u16>,
|
||||
generate_audio: Option<bool>,
|
||||
start_frame_token: Option<MediaFileToken>,
|
||||
) -> OmniGenVideoCostAndGenerateRequest {
|
||||
OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some("11111111-1111-1111-1111-111111111111".to_string()),
|
||||
model: Some(CommonVideoModel::Veo3),
|
||||
prompt: prompt.map(|s| s.to_string()),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: start_frame_token,
|
||||
end_frame_image_media_token: None,
|
||||
reference_image_media_tokens: None,
|
||||
reference_video_media_tokens: None,
|
||||
reference_audio_media_tokens: None,
|
||||
reference_character_tokens: None,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
quality: None,
|
||||
duration_seconds,
|
||||
video_batch_count: None,
|
||||
generate_audio,
|
||||
}
|
||||
}
|
||||
|
||||
fn fake_token(name: &str) -> (MediaFileToken, HashMap<MediaFileToken, Url>) {
|
||||
let token = MediaFileToken::new_from_str(name);
|
||||
let url = Url::parse(&format!("https://fake.example.com/{}.png", name)).unwrap();
|
||||
let mut map = HashMap::new();
|
||||
map.insert(token.clone(), url);
|
||||
(token, map)
|
||||
}
|
||||
|
||||
fn distill_text(request: &OmniGenVideoCostAndGenerateRequest) -> DistilledVideoRequest {
|
||||
let empty: HashMap<MediaFileToken, Url> = HashMap::new();
|
||||
distill_video_request(request, Some(&empty), Provider::Fal)
|
||||
.expect("distill should succeed for Veo 3 (text)")
|
||||
}
|
||||
|
||||
fn distill_image(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
hydration: &HashMap<MediaFileToken, Url>,
|
||||
) -> DistilledVideoRequest {
|
||||
distill_video_request(request, Some(hydration), Provider::Fal)
|
||||
.expect("distill should succeed for Veo 3 (image)")
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// COST
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Artcraft cost path: always 8s (legacy). $0.20/sec no-audio, $0.40/sec audio.
|
||||
// Default audio = false (artcraft legacy).
|
||||
mod cost {
|
||||
use super::*;
|
||||
|
||||
mod text {
|
||||
use super::*;
|
||||
|
||||
fn cost(duration: Option<u16>, audio: Option<bool>) -> u64 {
|
||||
let request = make_request(Some("p"), None, None, duration, audio, None);
|
||||
distill_text(&request).cost.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// ── No audio: $0.20/sec ──────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_no_audio_costs_160() { assert_eq!(cost(None, None), 160); } // 8s × 20¢
|
||||
|
||||
#[test]
|
||||
fn four_seconds_no_audio_costs_80() { assert_eq!(cost(Some(4), Some(false)), 80); }
|
||||
|
||||
#[test]
|
||||
fn six_seconds_no_audio_costs_120() { assert_eq!(cost(Some(6), Some(false)), 120); }
|
||||
|
||||
#[test]
|
||||
fn eight_seconds_no_audio_costs_160() { assert_eq!(cost(Some(8), Some(false)), 160); }
|
||||
|
||||
// ── Audio on: $0.40/sec ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn four_seconds_audio_on_costs_160() { assert_eq!(cost(Some(4), Some(true)), 160); }
|
||||
|
||||
#[test]
|
||||
fn six_seconds_audio_on_costs_240() { assert_eq!(cost(Some(6), Some(true)), 240); }
|
||||
|
||||
#[test]
|
||||
fn eight_seconds_audio_on_costs_320() { assert_eq!(cost(Some(8), Some(true)), 320); }
|
||||
|
||||
#[test]
|
||||
fn default_duration_audio_on_costs_320() { assert_eq!(cost(None, Some(true)), 320); }
|
||||
|
||||
// ── Over-max clamps to 8s ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_to_160_no_audio() { assert_eq!(cost(Some(20), Some(false)), 160); }
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_to_320_audio() { assert_eq!(cost(Some(20), Some(true)), 320); }
|
||||
|
||||
// ── Cost is independent of aspect ratio ───────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cost_independent_of_aspect_ratio() {
|
||||
for ar in [None, Some(CommonAspectRatio::Auto), Some(CommonAspectRatio::WideSixteenByNine), Some(CommonAspectRatio::TallNineBySixteen)] {
|
||||
let request = make_request(Some("p"), ar, None, None, None, None);
|
||||
assert_eq!(distill_text(&request).cost.cost_in_usd_cents.unwrap(), 160);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cost is independent of resolution ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cost_independent_of_resolution() {
|
||||
for res in [None, Some(CommonResolution::SevenTwentyP), Some(CommonResolution::TenEightyP)] {
|
||||
let request = make_request(Some("p"), None, res, None, None, None);
|
||||
assert_eq!(distill_text(&request).cost.cost_in_usd_cents.unwrap(), 160);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Metadata flags ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cost_metadata_flags_are_default() {
|
||||
let d = distill_text(&make_request(Some("p"), None, None, None, None, None));
|
||||
assert!(!d.cost.is_free);
|
||||
assert!(!d.cost.is_unlimited);
|
||||
assert!(!d.cost.is_rate_limited);
|
||||
assert!(!d.cost.has_watermark);
|
||||
}
|
||||
}
|
||||
|
||||
mod image {
|
||||
use super::*;
|
||||
|
||||
fn cost_img(duration: Option<u16>, audio: Option<bool>) -> u64 {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, duration, audio, Some(start));
|
||||
distill_image(&request, &map).cost.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_costs_160() { assert_eq!(cost_img(None, None), 160); }
|
||||
|
||||
#[test]
|
||||
fn four_seconds_no_audio_costs_80() { assert_eq!(cost_img(Some(4), Some(false)), 80); }
|
||||
|
||||
#[test]
|
||||
fn six_seconds_audio_on_costs_240() { assert_eq!(cost_img(Some(6), Some(true)), 240); }
|
||||
|
||||
#[test]
|
||||
fn eight_seconds_audio_on_costs_320() { assert_eq!(cost_img(Some(8), Some(true)), 320); }
|
||||
|
||||
#[test]
|
||||
fn image_and_text_cost_match() {
|
||||
let text = distill_text(&make_request(Some("p"), None, None, None, None, None))
|
||||
.cost.cost_in_usd_cents.unwrap();
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let img = distill_image(
|
||||
&make_request(Some("p"), None, None, None, None, Some(start)),
|
||||
&map,
|
||||
).cost.cost_in_usd_cents.unwrap();
|
||||
assert_eq!(text, img);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// PLAN
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
mod plan {
|
||||
use super::*;
|
||||
|
||||
use artcraft_router::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_router::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3::{
|
||||
FalVeo3Duration, FalVeo3I2vAspectRatio, FalVeo3Mode, FalVeo3Resolution,
|
||||
FalVeo3T2vAspectRatio, PlanFalVeo3,
|
||||
};
|
||||
|
||||
fn with_text_plan<F: FnOnce(&PlanFalVeo3)>(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
assertion: F,
|
||||
) {
|
||||
let distilled = distill_text(request);
|
||||
match distilled.plan() {
|
||||
VideoGenerationPlan::FalVeo3(plan) => assertion(plan),
|
||||
other => panic!("expected FalVeo3, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_image_plan<F: FnOnce(&PlanFalVeo3)>(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
hydration: &HashMap<MediaFileToken, Url>,
|
||||
assertion: F,
|
||||
) {
|
||||
let distilled = distill_image(request, hydration);
|
||||
match distilled.plan() {
|
||||
VideoGenerationPlan::FalVeo3(plan) => assertion(plan),
|
||||
other => panic!("expected FalVeo3, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
mod text {
|
||||
use super::*;
|
||||
|
||||
// ── Mode detection ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn text_mode_is_text_to_video() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.mode, FalVeo3Mode::TextToVideo));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Prompt passthrough ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
with_text_plan(&make_request(Some("a horse on mars"), None, None, None, None, None), |plan| {
|
||||
assert_eq!(plan.prompt, "a horse on mars");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prompt_is_empty_string() {
|
||||
with_text_plan(&make_request(None, None, None, None, None, None), |plan| {
|
||||
assert_eq!(plan.prompt, "");
|
||||
});
|
||||
}
|
||||
|
||||
// ── Aspect ratio mappings (text mode only) ────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_aspect_ratio_is_default() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.t2v_aspect_ratio, Some(FalVeo3T2vAspectRatio::Default)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_16x9_yields_wide() {
|
||||
for ar in [CommonAspectRatio::WideSixteenByNine, CommonAspectRatio::Wide] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.t2v_aspect_ratio, Some(FalVeo3T2vAspectRatio::WideSixteenNine)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tall_9x16_yields_tall() {
|
||||
for ar in [CommonAspectRatio::TallNineBySixteen, CommonAspectRatio::Tall] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.t2v_aspect_ratio, Some(FalVeo3T2vAspectRatio::TallNineSixteen)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square_falls_back_to_default() {
|
||||
// Veo 3 t2v has no Square — falls back to Default (16:9).
|
||||
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.t2v_aspect_ratio, Some(FalVeo3T2vAspectRatio::Default)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_falls_back_to_default() {
|
||||
// Veo 3 t2v has no Auto — falls back to Default (16:9).
|
||||
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.t2v_aspect_ratio, Some(FalVeo3T2vAspectRatio::Default)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_aspect_ratio_falls_back_to_default() {
|
||||
for ar in [CommonAspectRatio::WideFourByThree, CommonAspectRatio::TallThreeByFour, CommonAspectRatio::WideFiveByFour] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.t2v_aspect_ratio, Some(FalVeo3T2vAspectRatio::Default)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Duration mappings ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_duration_is_default() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::Default));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_4_yields_four() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, Some(4), None, None), |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::FourSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_6_yields_six() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, Some(6), None, None), |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::SixSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_8_yields_eight() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, Some(8), None, None), |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_to_eight() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, Some(20), None, None), |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Resolution mappings ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_resolution_is_default() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.resolution, FalVeo3Resolution::Default));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_twenty_p_yields_seven_twenty_p() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(CommonResolution::SevenTwentyP), None, None, None), |plan| {
|
||||
assert!(matches!(plan.resolution, FalVeo3Resolution::SevenTwentyP));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_eighty_p_yields_ten_eighty_p() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(CommonResolution::TenEightyP), None, None, None), |plan| {
|
||||
assert!(matches!(plan.resolution, FalVeo3Resolution::TenEightyP));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Generate audio passthrough ────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_audio_is_true() {
|
||||
// Fal plan defaults generate_audio to true.
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, None, None), |plan| {
|
||||
assert!(plan.generate_audio);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_true_passes_through() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, Some(true), None), |plan| {
|
||||
assert!(plan.generate_audio);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_false_passes_through() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, Some(false), None), |plan| {
|
||||
assert!(!plan.generate_audio);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mod image {
|
||||
use super::*;
|
||||
|
||||
// ── Mode detection / start frame hydration ────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn start_frame_triggers_image_to_video() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
match &plan.mode {
|
||||
FalVeo3Mode::ImageToVideo { image_url } => {
|
||||
assert!(image_url.starts_with("https://fake.example.com/"));
|
||||
}
|
||||
other => panic!("expected ImageToVideo, got {:?}", other),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_frame_url_matches_hydration_map_exactly() {
|
||||
let token = MediaFileToken::new_from_str("mf_myframe0000000000000000000000");
|
||||
let expected_url = "https://cdn.example.com/frames/my_frame.png";
|
||||
let mut map = HashMap::new();
|
||||
map.insert(token.clone(), Url::parse(expected_url).unwrap());
|
||||
|
||||
let request = make_request(Some("p"), None, None, None, None, Some(token));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
match &plan.mode {
|
||||
FalVeo3Mode::ImageToVideo { image_url } => {
|
||||
assert_eq!(image_url, expected_url);
|
||||
}
|
||||
other => panic!("expected ImageToVideo, got {:?}", other),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Aspect ratio is None in image mode ────────────────────────────────
|
||||
|
||||
// ── Aspect ratio in image mode: uses i2v (has Auto, no Square) ─────────
|
||||
|
||||
#[test]
|
||||
fn t2v_aspect_ratio_is_none_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(CommonAspectRatio::WideSixteenByNine), None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(plan.t2v_aspect_ratio.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_default_aspect_ratio_is_auto() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.i2v_aspect_ratio, Some(FalVeo3I2vAspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_wide_16x9_yields_wide() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(CommonAspectRatio::WideSixteenByNine), None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.i2v_aspect_ratio, Some(FalVeo3I2vAspectRatio::WideSixteenNine)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_tall_9x16_yields_tall() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(CommonAspectRatio::TallNineBySixteen), None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.i2v_aspect_ratio, Some(FalVeo3I2vAspectRatio::TallNineSixteen)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_auto_yields_auto() {
|
||||
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(ar), None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.i2v_aspect_ratio, Some(FalVeo3I2vAspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_square_falls_back_to_auto() {
|
||||
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(ar), None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.i2v_aspect_ratio, Some(FalVeo3I2vAspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_unsupported_falls_back_to_auto() {
|
||||
for ar in [CommonAspectRatio::WideFourByThree, CommonAspectRatio::TallThreeByFour] {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(ar), None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.i2v_aspect_ratio, Some(FalVeo3I2vAspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Prompt passthrough in image mode ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("a horse on mars"), None, None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert_eq!(plan.prompt, "a horse on mars");
|
||||
});
|
||||
}
|
||||
|
||||
// ── Duration pass-throughs in image mode ──────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn duration_4_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, Some(4), None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::FourSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_8_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, Some(8), None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_duration_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::Default));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Resolution in image mode ──────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn resolution_passes_through_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, Some(CommonResolution::TenEightyP), None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.resolution, FalVeo3Resolution::TenEightyP));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Audio in image mode ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn audio_passes_through_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, None, Some(false), Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(!plan.generate_audio);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_audio_is_true_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(plan.generate_audio);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user