mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-23 18:19:30 -04:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a569947608 | ||
|
|
9d66a880f9 | ||
|
|
7c2b0f310e |
@@ -36,7 +36,6 @@ script/socat_dev_database_connect.sh
|
||||
venv/
|
||||
|
||||
# Testing
|
||||
/prompts/
|
||||
/requests/
|
||||
|
||||
# Scratch data
|
||||
|
||||
Generated
-23
@@ -797,7 +797,6 @@ dependencies = [
|
||||
"tokio",
|
||||
"uuid 1.18.1",
|
||||
"workspace-build-acceleration",
|
||||
"world_labs_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4186,7 +4185,6 @@ dependencies = [
|
||||
"log",
|
||||
"reqwest 0.12.23",
|
||||
"serde",
|
||||
"strum 0.28.0",
|
||||
"test_data",
|
||||
"testing",
|
||||
"tokio",
|
||||
@@ -11604,7 +11602,6 @@ dependencies = [
|
||||
"utoipa-swagger-ui",
|
||||
"videos",
|
||||
"workspace-build-acceleration",
|
||||
"world_labs_api",
|
||||
"wreq",
|
||||
"zip 0.6.6",
|
||||
]
|
||||
@@ -15009,26 +15006,6 @@ dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "world_labs_api"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"env",
|
||||
"env_logger 0.11.9",
|
||||
"errors",
|
||||
"filesys",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.28.0",
|
||||
"tokio",
|
||||
"uuid 1.18.1",
|
||||
"workspace-build-acceleration",
|
||||
"wreq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "world_labs_client"
|
||||
version = "0.0.1"
|
||||
|
||||
@@ -19,7 +19,6 @@ members = [
|
||||
"crates/api_clients/seedance2pro",
|
||||
"crates/api_clients/runwayml_client",
|
||||
"crates/api_clients/thumbnail_generator",
|
||||
"crates/api_clients/world_labs_api",
|
||||
"crates/api_clients/world_labs_client",
|
||||
|
||||
# CLI tools for operations, etc.
|
||||
@@ -249,7 +248,6 @@ tokens = { path = "crates/schema/public/tokens" }
|
||||
url_utils = { path = "crates/lib/url_utils" }
|
||||
users = { path = "crates/lib/users" }
|
||||
web_base64 = { path = "crates/lib/data/web_base64" }
|
||||
world_labs_api = { path = "crates/api_clients/world_labs_api" }
|
||||
world_labs_client = { path = "crates/api_clients/world_labs_client" }
|
||||
|
||||
# External libs
|
||||
|
||||
@@ -55,7 +55,7 @@ We haven't abandoned text-to-asset generation for quick prototyping and ideation
|
||||
|
||||
| Provider | Features |
|
||||
|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Artcraft | Nano Banana, Nano Banana Pro, GPT-Image-1 / 1.5, Seedance 1.0 Lite / 1.5 Pro / 2.0, Seedream 4 / 4.5, Flux 1.1 / Kontext, Veo 2 / 3 / 3.1, Kling 1.6 / 2.1 / 2.5 / 2.6, Sora 2 / Pro, Hunyuan 3d 2 / 3 |
|
||||
| Artcraft | Nano Banana, Nano Banana Pro, GPT-Image-1 / 1.5, Seedance 2 / Seedream 4 / 4.5, Flux 1.1 / Kontext, Veo 2 / 3 / 3.1, Kling 1.6 / 2.1 / 2.5 / 2.6, Seedance, Sora 2 / Pro, Hunyuan 3d 2 / 3 |
|
||||
| Grok | Grok Imagine, Grok Video |
|
||||
| Midjourney | Image Gen (all versions) |
|
||||
| Sora | Sora 1, Sora 2, GPT-Image-1 |
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE web_referrals
|
||||
MODIFY COLUMN utm_source VARCHAR(32) DEFAULT NULL,
|
||||
MODIFY COLUMN utm_medium VARCHAR(32) DEFAULT NULL,
|
||||
MODIFY COLUMN utm_campaign VARCHAR(255) DEFAULT NULL;
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE web_referrals
|
||||
MODIFY COLUMN utm_source VARCHAR(150) DEFAULT NULL,
|
||||
MODIFY COLUMN utm_medium VARCHAR(150) DEFAULT NULL,
|
||||
MODIFY COLUMN utm_campaign VARCHAR(150) DEFAULT NULL;
|
||||
@@ -13,11 +13,11 @@ CREATE TABLE web_referrals (
|
||||
-- The parsed out referral domain
|
||||
domain VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
utm_source VARCHAR(150) DEFAULT NULL,
|
||||
utm_source VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
utm_medium VARCHAR(150) DEFAULT NULL,
|
||||
utm_medium VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
utm_campaign VARCHAR(150) DEFAULT NULL,
|
||||
utm_campaign VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
ip_address VARCHAR(40) NOT NULL,
|
||||
|
||||
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::common::generation::common_splat_model::CommonSplatModel;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
|
||||
pub const ESTIMATE_SPLAT_COST_PATH: &str = "/v1/generate/cost_estimate/splat";
|
||||
|
||||
/// Request body for the splat cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateSplatCostRequest {
|
||||
/// The splat model to estimate costs for.
|
||||
pub model: CommonSplatModel,
|
||||
|
||||
/// The provider to route the generation through.
|
||||
pub provider: GenerationProvider,
|
||||
|
||||
/// Whether the request includes a reference image.
|
||||
pub has_reference_image: Option<bool>,
|
||||
}
|
||||
|
||||
/// Response body for the splat cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateSplatCostResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// Estimated cost in credits.
|
||||
pub cost_in_credits: Option<u64>,
|
||||
|
||||
/// Estimated cost in USD cents.
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
|
||||
/// Whether the generation is free for this user/plan.
|
||||
pub is_free: bool,
|
||||
|
||||
/// Whether the user has unlimited generations.
|
||||
pub is_unlimited: bool,
|
||||
|
||||
/// Whether the user is rate limited.
|
||||
pub is_rate_limited: bool,
|
||||
|
||||
/// Whether the output will have a watermark.
|
||||
pub has_watermark: bool,
|
||||
}
|
||||
|
||||
/// Error response for the splat cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateSplatCostError {
|
||||
pub success: bool,
|
||||
pub error_type: EstimateSplatCostErrorType,
|
||||
pub error_message: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EstimateSplatCostErrorType {
|
||||
InvalidProviderForModel,
|
||||
InvalidInput,
|
||||
}
|
||||
+2
-10
@@ -9,10 +9,7 @@ use enums::common::generation_provider::GenerationProvider;
|
||||
pub const ESTIMATE_VIDEO_COST_PATH: &str = "/v1/generate/cost_estimate/video";
|
||||
|
||||
/// Request body for the video cost estimate endpoint.
|
||||
///
|
||||
/// Every model cares about its own set of parameters and not all
|
||||
/// parameters may be relevant for every model.
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateVideoCostRequest {
|
||||
/// The video model to estimate costs for.
|
||||
pub model: CommonVideoModel,
|
||||
@@ -37,13 +34,10 @@ pub struct EstimateVideoCostRequest {
|
||||
|
||||
/// Number of videos to generate in parallel.
|
||||
pub video_batch_count: Option<u16>,
|
||||
|
||||
/// Whether to generate audio for the video.
|
||||
pub generate_audio: Option<bool>,
|
||||
}
|
||||
|
||||
/// Describes the type of video generation being requested.
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum GenerationMode {
|
||||
TextToVideo,
|
||||
@@ -105,7 +99,6 @@ mod tests {
|
||||
resolution: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
};
|
||||
let serialized = serde_json::to_string(&request).unwrap();
|
||||
assert!(serialized.contains("\"generation_mode\":{\"type\":\"text_to_video\"}"));
|
||||
@@ -122,7 +115,6 @@ mod tests {
|
||||
resolution: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
};
|
||||
let serialized = serde_json::to_string(&request).unwrap();
|
||||
assert!(serialized.contains("\"generation_mode\":{\"type\":\"reference_image_to_video\",\"count\":1}"));
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod estimate_image_cost;
|
||||
pub mod estimate_splat_cost;
|
||||
pub mod estimate_video_cost;
|
||||
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const FLUX_2_LORA_EDIT_IMAGE_ANGLE_PATH: &str = "/v1/generate/image/angle/flux_2_lora";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct Flux2LoraEditImageAngleRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// The image to edit with angle adjustment.
|
||||
pub image_media_token: MediaFileToken,
|
||||
|
||||
/// Horizontal rotation angle in degrees.
|
||||
pub horizontal_angle: Option<f64>,
|
||||
|
||||
/// Vertical rotation angle in degrees.
|
||||
pub vertical_angle: Option<f64>,
|
||||
|
||||
/// Zoom level.
|
||||
pub zoom: Option<f64>,
|
||||
|
||||
/// Number of images to generate. Default is one.
|
||||
pub num_images: Option<Flux2LoraEditImageAngleNumImages>,
|
||||
|
||||
/// Output image size. Default is square_hd.
|
||||
pub image_size: Option<Flux2LoraEditImageAngleImageSize>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Flux2LoraEditImageAngleNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Flux2LoraEditImageAngleImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct Flux2LoraEditImageAngleResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod flux_2_lora_edit_image_angle;
|
||||
pub mod qwen_edit_2511_edit_image_angle;
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const QWEN_EDIT_2511_EDIT_IMAGE_ANGLE_PATH: &str = "/v1/generate/image/angle/qwen_edit_2511";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct QwenEdit2511EditImageAngleRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// The image to edit with angle adjustment.
|
||||
pub image_media_token: MediaFileToken,
|
||||
|
||||
/// Horizontal rotation angle in degrees.
|
||||
pub horizontal_angle: Option<f64>,
|
||||
|
||||
/// Vertical rotation angle in degrees.
|
||||
pub vertical_angle: Option<f64>,
|
||||
|
||||
/// Zoom level.
|
||||
pub zoom: Option<f64>,
|
||||
|
||||
/// Additional text prompt to guide the generation.
|
||||
pub additional_prompt: Option<String>,
|
||||
|
||||
/// Number of images to generate. Default is one.
|
||||
pub num_images: Option<QwenEdit2511EditImageAngleNumImages>,
|
||||
|
||||
/// Output image size. Default is square_hd.
|
||||
pub image_size: Option<QwenEdit2511EditImageAngleImageSize>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QwenEdit2511EditImageAngleNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QwenEdit2511EditImageAngleImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct QwenEdit2511EditImageAngleResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod angle;
|
||||
pub mod bg_removal;
|
||||
pub mod edit;
|
||||
pub mod inpaint;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod cost_estimate;
|
||||
pub mod image;
|
||||
pub mod object;
|
||||
pub mod splat;
|
||||
pub mod video;
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const GENERATE_WORLDLABS_MARBLE_0P1_MINI_SPLAT_URL_PATH: &str = "/v1/generate/splat/worldlabs_marble_0p1_mini";
|
||||
|
||||
/// World Labs Marble 0.1-mini Splat Generation
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateWorldlabsMarble0p1MiniSplatRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Optional image to use as input for world generation.
|
||||
pub image_media_file_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional text prompt for world generation.
|
||||
pub prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateWorldlabsMarble0p1MiniSplatResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const GENERATE_WORLDLABS_MARBLE_0P1_PLUS_SPLAT_URL_PATH: &str = "/v1/generate/splat/worldlabs_marble_0p1_plus";
|
||||
|
||||
/// World Labs Marble 0.1-plus Splat Generation
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateWorldlabsMarble0p1PlusSplatRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Optional image to use as input for world generation.
|
||||
pub image_media_file_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional text prompt for world generation.
|
||||
pub prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateWorldlabsMarble0p1PlusSplatResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod generate_worldlabs_marble_0p1_mini_splat;
|
||||
pub mod generate_worldlabs_marble_0p1_plus_splat;
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const KLING_3P0_PRO_MULTI_FUNCTION_VIDEO_PATH: &str = "/v1/generate/video/multi_function/kling_3p0_pro";
|
||||
|
||||
/// Both text-to-video and image-to-video in one request.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct Kling3p0ProMultiFunctionVideoGenRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Text prompt.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Optional negative prompt.
|
||||
pub negative_prompt: Option<String>,
|
||||
|
||||
/// If present, we're doing image-to-video. If absent, text-to-video.
|
||||
pub image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional end frame image.
|
||||
pub end_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Duration of the video (3-15 seconds).
|
||||
pub duration: Option<Kling3p0ProMultiFunctionVideoGenDuration>,
|
||||
|
||||
/// Whether to generate audio.
|
||||
pub generate_audio: Option<bool>,
|
||||
|
||||
/// Aspect ratio (only for text-to-video).
|
||||
pub aspect_ratio: Option<Kling3p0ProMultiFunctionVideoGenAspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Kling3p0ProMultiFunctionVideoGenDuration {
|
||||
ThreeSeconds,
|
||||
FourSeconds,
|
||||
FiveSeconds,
|
||||
SixSeconds,
|
||||
SevenSeconds,
|
||||
EightSeconds,
|
||||
NineSeconds,
|
||||
TenSeconds,
|
||||
ElevenSeconds,
|
||||
TwelveSeconds,
|
||||
ThirteenSeconds,
|
||||
FourteenSeconds,
|
||||
FifteenSeconds,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Kling3p0ProMultiFunctionVideoGenAspectRatio {
|
||||
Square,
|
||||
SixteenByNine,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct Kling3p0ProMultiFunctionVideoGenResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const KLING_3P0_STANDARD_MULTI_FUNCTION_VIDEO_PATH: &str = "/v1/generate/video/multi_function/kling_3p0_standard";
|
||||
|
||||
/// Both text-to-video and image-to-video in one request.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct Kling3p0StandardMultiFunctionVideoGenRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Text prompt.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Optional negative prompt.
|
||||
pub negative_prompt: Option<String>,
|
||||
|
||||
/// If present, we're doing image-to-video. If absent, text-to-video.
|
||||
pub image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional end frame image.
|
||||
pub end_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Duration of the video (3-15 seconds).
|
||||
pub duration: Option<Kling3p0StandardMultiFunctionVideoGenDuration>,
|
||||
|
||||
/// Whether to generate audio.
|
||||
pub generate_audio: Option<bool>,
|
||||
|
||||
/// Aspect ratio (only for text-to-video).
|
||||
pub aspect_ratio: Option<Kling3p0StandardMultiFunctionVideoGenAspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Kling3p0StandardMultiFunctionVideoGenDuration {
|
||||
ThreeSeconds,
|
||||
FourSeconds,
|
||||
FiveSeconds,
|
||||
SixSeconds,
|
||||
SevenSeconds,
|
||||
EightSeconds,
|
||||
NineSeconds,
|
||||
TenSeconds,
|
||||
ElevenSeconds,
|
||||
TwelveSeconds,
|
||||
ThirteenSeconds,
|
||||
FourteenSeconds,
|
||||
FifteenSeconds,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Kling3p0StandardMultiFunctionVideoGenAspectRatio {
|
||||
Square,
|
||||
SixteenByNine,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct Kling3p0StandardMultiFunctionVideoGenResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
pub mod kling_2_5_turbo_multi_function_video_gen;
|
||||
pub mod kling_2_6_multi_function_video_gen;
|
||||
pub mod kling_3p0_pro_multi_function_video_gen;
|
||||
pub mod kling_3p0_standard_multi_function_video_gen;
|
||||
pub mod seedance_1p5_pro_multi_function_video_gen;
|
||||
pub mod seedance_2p0_multi_function_video_gen;
|
||||
pub mod sora_2_multi_function_video_gen;
|
||||
pub mod sora_2_pro_multi_function_video_gen;
|
||||
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const SEEDANCE_1P5_PRO_MULTI_FUNCTION_VIDEO_GEN_PATH: &str = "/v1/generate/video/multi_function/seedance_1p5_pro";
|
||||
|
||||
/// Both text-to-video and image-to-video in one request.
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct Seedance1p5ProMultiFunctionVideoGenRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// "Required".
|
||||
/// Required downstream, but we'll coerce null to empty string.
|
||||
/// Text prompt.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Optional.
|
||||
/// Only for image-to-video.
|
||||
/// Source of the starting frame.
|
||||
/// If present, we're doing image-to-video.
|
||||
/// If absent, we're doing text-to-video.
|
||||
pub start_frame_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional.
|
||||
/// Only for image-to-video.
|
||||
/// Source of the ending frame.
|
||||
pub end_frame_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional.
|
||||
pub resolution: Option<Seedance1p5ProMultiFunctionVideoGenResolution>,
|
||||
|
||||
/// Optional.
|
||||
/// Duration of the video.
|
||||
/// (this is uniform against all modes)
|
||||
pub duration: Option<Seedance1p5ProMultiFunctionVideoGenDuration>,
|
||||
|
||||
/// Optional.
|
||||
pub aspect_ratio: Option<Seedance1p5ProMultiFunctionVideoGenAspectRatio>,
|
||||
|
||||
/// Whether to generate audio.
|
||||
pub generate_audio: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Copy, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Seedance1p5ProMultiFunctionVideoGenResolution {
|
||||
FourEightyP,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Copy, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Seedance1p5ProMultiFunctionVideoGenDuration {
|
||||
FourSeconds,
|
||||
FiveSeconds,
|
||||
SixSeconds,
|
||||
SevenSeconds,
|
||||
EightSeconds,
|
||||
NineSeconds,
|
||||
TenSeconds,
|
||||
ElevenSeconds,
|
||||
TwelveSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Copy, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Seedance1p5ProMultiFunctionVideoGenAspectRatio {
|
||||
TwentyOneByNine,
|
||||
SixteenByNine,
|
||||
FourByThree,
|
||||
Square,
|
||||
ThreeByFour,
|
||||
NineBySixteen,
|
||||
Auto,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct Seedance1p5ProMultiFunctionVideoGenResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
-1
@@ -1,3 +1,2 @@
|
||||
pub mod image;
|
||||
pub mod splat;
|
||||
pub mod video;
|
||||
|
||||
-20
@@ -1,20 +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::generate::cost_estimate::estimate_splat_cost::{
|
||||
EstimateSplatCostRequest, EstimateSplatCostResponse, ESTIMATE_SPLAT_COST_PATH,
|
||||
};
|
||||
|
||||
pub async fn estimate_splat_cost(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: EstimateSplatCostRequest,
|
||||
) -> Result<EstimateSplatCostResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
ESTIMATE_SPLAT_COST_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
pub mod estimate_splat_cost;
|
||||
-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::generate::image::angle::flux_2_lora_edit_image_angle::{
|
||||
Flux2LoraEditImageAngleRequest, Flux2LoraEditImageAngleResponse,
|
||||
FLUX_2_LORA_EDIT_IMAGE_ANGLE_PATH,
|
||||
};
|
||||
|
||||
pub async fn flux_2_lora_edit_image_angle(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: Flux2LoraEditImageAngleRequest,
|
||||
) -> Result<Flux2LoraEditImageAngleResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
FLUX_2_LORA_EDIT_IMAGE_ANGLE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod flux_2_lora_edit_image_angle;
|
||||
pub mod qwen_edit_2511_edit_image_angle;
|
||||
-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::generate::image::angle::qwen_edit_2511_edit_image_angle::{
|
||||
QwenEdit2511EditImageAngleRequest, QwenEdit2511EditImageAngleResponse,
|
||||
QWEN_EDIT_2511_EDIT_IMAGE_ANGLE_PATH,
|
||||
};
|
||||
|
||||
pub async fn qwen_edit_2511_edit_image_angle(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: QwenEdit2511EditImageAngleRequest,
|
||||
) -> Result<QwenEdit2511EditImageAngleResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
QWEN_EDIT_2511_EDIT_IMAGE_ANGLE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod angle;
|
||||
pub mod bg_removal;
|
||||
pub mod edit;
|
||||
pub mod inpaint;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod cost_estimate;
|
||||
pub mod image;
|
||||
pub mod object;
|
||||
pub mod splat;
|
||||
pub mod video;
|
||||
-18
@@ -1,18 +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::generate::splat::generate_worldlabs_marble_0p1_mini_splat::{GenerateWorldlabsMarble0p1MiniSplatRequest, GenerateWorldlabsMarble0p1MiniSplatResponse, GENERATE_WORLDLABS_MARBLE_0P1_MINI_SPLAT_URL_PATH};
|
||||
|
||||
pub async fn generate_worldlabs_marble_0p1_mini_splat(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateWorldlabsMarble0p1MiniSplatRequest,
|
||||
) -> Result<GenerateWorldlabsMarble0p1MiniSplatResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_WORLDLABS_MARBLE_0P1_MINI_SPLAT_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
-18
@@ -1,18 +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::generate::splat::generate_worldlabs_marble_0p1_plus_splat::{GenerateWorldlabsMarble0p1PlusSplatRequest, GenerateWorldlabsMarble0p1PlusSplatResponse, GENERATE_WORLDLABS_MARBLE_0P1_PLUS_SPLAT_URL_PATH};
|
||||
|
||||
pub async fn generate_worldlabs_marble_0p1_plus_splat(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateWorldlabsMarble0p1PlusSplatRequest,
|
||||
) -> Result<GenerateWorldlabsMarble0p1PlusSplatResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_WORLDLABS_MARBLE_0P1_PLUS_SPLAT_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod generate_worldlabs_marble_0p1_mini_splat;
|
||||
pub mod generate_worldlabs_marble_0p1_plus_splat;
|
||||
-18
@@ -1,18 +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::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::{Kling3p0ProMultiFunctionVideoGenRequest, Kling3p0ProMultiFunctionVideoGenResponse, KLING_3P0_PRO_MULTI_FUNCTION_VIDEO_PATH};
|
||||
|
||||
pub async fn kling_3p0_pro_multi_function_video_gen(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: Kling3p0ProMultiFunctionVideoGenRequest,
|
||||
) -> Result<Kling3p0ProMultiFunctionVideoGenResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
KLING_3P0_PRO_MULTI_FUNCTION_VIDEO_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
-18
@@ -1,18 +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::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::{Kling3p0StandardMultiFunctionVideoGenRequest, Kling3p0StandardMultiFunctionVideoGenResponse, KLING_3P0_STANDARD_MULTI_FUNCTION_VIDEO_PATH};
|
||||
|
||||
pub async fn kling_3p0_standard_multi_function_video_gen(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: Kling3p0StandardMultiFunctionVideoGenRequest,
|
||||
) -> Result<Kling3p0StandardMultiFunctionVideoGenResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
KLING_3P0_STANDARD_MULTI_FUNCTION_VIDEO_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
-3
@@ -1,9 +1,6 @@
|
||||
pub mod kling_2p5_turbo_pro_multi_function_video_gen;
|
||||
pub mod seedance_1p5_pro_multi_function_video_gen;
|
||||
pub mod seedance_2p0_multi_function_video_gen;
|
||||
pub mod kling_2p6_pro_multi_function_video_gen;
|
||||
pub mod kling_3p0_pro_multi_function_video_gen;
|
||||
pub mod kling_3p0_standard_multi_function_video_gen;
|
||||
pub mod sora_2_multi_function_video_gen;
|
||||
pub mod sora_2_pro_multi_function_video_gen;
|
||||
pub mod veo_3p1_fast_multi_function_video_gen;
|
||||
|
||||
-18
@@ -1,18 +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::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::{Seedance1p5ProMultiFunctionVideoGenRequest, Seedance1p5ProMultiFunctionVideoGenResponse, SEEDANCE_1P5_PRO_MULTI_FUNCTION_VIDEO_GEN_PATH};
|
||||
|
||||
pub async fn seedance_1p5_pro_multi_function_video_gen(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: Seedance1p5ProMultiFunctionVideoGenRequest,
|
||||
) -> Result<Seedance1p5ProMultiFunctionVideoGenResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
SEEDANCE_1P5_PRO_MULTI_FUNCTION_VIDEO_GEN_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -17,11 +17,10 @@ workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
artcraft_api_defs.workspace = true
|
||||
artcraft_client.workspace = true
|
||||
fal_client.workspace = true
|
||||
seedance2pro.workspace = true
|
||||
artcraft_client.workspace = true
|
||||
tokens.workspace = true
|
||||
world_labs_api.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -27,8 +27,4 @@ pub enum CommonImageModel {
|
||||
Seedream4p5,
|
||||
#[serde(rename = "seedream_5_lite")]
|
||||
Seedream5Lite,
|
||||
#[serde(rename = "qwen_edit_2511_angles")]
|
||||
QwenEdit2511Angles,
|
||||
#[serde(rename = "flux_2_lora_angles")]
|
||||
Flux2LoraAngles,
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Common splat models supported by the router.
|
||||
/// Not all models are available through all providers.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonSplatModel {
|
||||
#[serde(rename = "marble_0p1_mini")]
|
||||
Marble0p1Mini,
|
||||
|
||||
#[serde(rename = "marble_0p1_plus")]
|
||||
Marble0p1Plus,
|
||||
}
|
||||
@@ -23,18 +23,9 @@ pub enum CommonVideoModel {
|
||||
#[serde(rename = "kling_2p6_pro")]
|
||||
Kling2p6Pro,
|
||||
|
||||
#[serde(rename = "kling_3p0_standard")]
|
||||
Kling3p0Standard,
|
||||
|
||||
#[serde(rename = "kling_3p0_pro")]
|
||||
Kling3p0Pro,
|
||||
|
||||
#[serde(rename = "seedance_1p0_lite")]
|
||||
Seedance10Lite,
|
||||
|
||||
#[serde(rename = "seedance_1p5_pro")]
|
||||
Seedance1p5Pro,
|
||||
|
||||
#[serde(rename = "seedance_2p0")]
|
||||
Seedance2p0,
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod common_aspect_ratio;
|
||||
pub mod common_image_model;
|
||||
pub mod common_resolution;
|
||||
pub mod common_splat_model;
|
||||
pub mod common_video_model;
|
||||
pub mod image_list_ref;
|
||||
pub mod image_ref;
|
||||
|
||||
-3
@@ -47,9 +47,6 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
-3
@@ -35,9 +35,6 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
let estimate = request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::Flux2LoraEditImageAngleNumImages;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_flux_2_lora_angles(
|
||||
plan: &PlanArtcraftFlux2LoraAngles<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
|
||||
let cost_per_image: u64 = 4;
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
Flux2LoraEditImageAngleNumImages::One => 1,
|
||||
Flux2LoraEditImageAngleNumImages::Two => 2,
|
||||
Flux2LoraEditImageAngleNumImages::Three => 3,
|
||||
Flux2LoraEditImageAngleNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
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_image_model::CommonImageModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
|
||||
let tokens = vec![MediaFileToken::new_from_str("test_token")];
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::Flux2LoraAngles,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
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 test_estimate_cost_usd_cents() {
|
||||
// $0.04/image = 4 cents each
|
||||
assert_eq!(estimate_usd_cents(1), 4);
|
||||
assert_eq!(estimate_usd_cents(2), 8);
|
||||
assert_eq!(estimate_usd_cents(3), 12);
|
||||
assert_eq!(estimate_usd_cents(4), 16);
|
||||
}
|
||||
}
|
||||
-3
@@ -47,9 +47,6 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
-3
@@ -47,9 +47,6 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
-3
@@ -86,9 +86,6 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
-3
@@ -47,9 +47,6 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
-3
@@ -55,9 +55,6 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
-3
@@ -54,9 +54,6 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::QwenEdit2511EditImageAngleNumImages;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_qwen_edit_2511_angles(
|
||||
plan: &PlanArtcraftQwenEdit2511Angles<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
|
||||
let cost_per_image: u64 = 4;
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
QwenEdit2511EditImageAngleNumImages::One => 1,
|
||||
QwenEdit2511EditImageAngleNumImages::Two => 2,
|
||||
QwenEdit2511EditImageAngleNumImages::Three => 3,
|
||||
QwenEdit2511EditImageAngleNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
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_image_model::CommonImageModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
|
||||
let tokens = vec![MediaFileToken::new_from_str("test_token")];
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::QwenEdit2511Angles,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_usd_cents() {
|
||||
// $0.04/image = 4 cents each
|
||||
assert_eq!(estimate_usd_cents(1), 4);
|
||||
assert_eq!(estimate_usd_cents(2), 8);
|
||||
assert_eq!(estimate_usd_cents(3), 12);
|
||||
assert_eq!(estimate_usd_cents(4), 16);
|
||||
}
|
||||
}
|
||||
-3
@@ -47,9 +47,6 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
-3
@@ -47,9 +47,6 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
-3
@@ -47,9 +47,6 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
-2
@@ -9,5 +9,3 @@ pub mod estimate_image_cost_artcraft_nano_banana_pro;
|
||||
pub mod estimate_image_cost_artcraft_seedream_4;
|
||||
pub mod estimate_image_cost_artcraft_seedream_4p5;
|
||||
pub mod estimate_image_cost_artcraft_seedream_5_lite;
|
||||
pub mod estimate_image_cost_artcraft_qwen_edit_2511_angles;
|
||||
pub mod estimate_image_cost_artcraft_flux_2_lora_angles;
|
||||
|
||||
-3
@@ -52,9 +52,6 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
-72
@@ -1,72 +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_image::generate_image_response::{
|
||||
ArtcraftImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
|
||||
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::Flux2LoraEditImageAngleRequest;
|
||||
use artcraft_client::endpoints::generate::image::angle::flux_2_lora_edit_image_angle::flux_2_lora_edit_image_angle;
|
||||
|
||||
pub async fn execute_artcraft_flux_2_lora_angles(
|
||||
plan: &PlanArtcraftFlux2LoraAngles<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = Flux2LoraEditImageAngleRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
image_media_token: plan.image_input.clone(),
|
||||
horizontal_angle: plan.horizontal_angle,
|
||||
vertical_angle: plan.vertical_angle,
|
||||
zoom: plan.zoom,
|
||||
num_images: Some(plan.num_images),
|
||||
image_size: plan.image_size,
|
||||
};
|
||||
|
||||
let response = flux_2_lora_edit_image_angle(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::{base_flux_2_lora_angles_image_request, get_artcraft_client};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_angle_edit_flux_2_lora() {
|
||||
let client = get_artcraft_client();
|
||||
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::SquareHd),
|
||||
image_batch_count: Some(1),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
horizontal_angle: Some(30.0),
|
||||
vertical_angle: Some(0.0),
|
||||
zoom: Some(1.0),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
|
||||
println!("Job token: {:?}", payload.inference_job_token);
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
}
|
||||
-74
@@ -1,74 +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_image::generate_image_response::{
|
||||
ArtcraftImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
|
||||
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::QwenEdit2511EditImageAngleRequest;
|
||||
use artcraft_client::endpoints::generate::image::angle::qwen_edit_2511_edit_image_angle::qwen_edit_2511_edit_image_angle;
|
||||
|
||||
pub async fn execute_artcraft_qwen_edit_2511_angles(
|
||||
plan: &PlanArtcraftQwenEdit2511Angles<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = QwenEdit2511EditImageAngleRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
image_media_token: plan.image_input.clone(),
|
||||
horizontal_angle: plan.horizontal_angle,
|
||||
vertical_angle: plan.vertical_angle,
|
||||
zoom: plan.zoom,
|
||||
additional_prompt: plan.prompt.map(|p| p.to_string()),
|
||||
num_images: Some(plan.num_images),
|
||||
image_size: plan.image_size,
|
||||
};
|
||||
|
||||
let response = qwen_edit_2511_edit_image_angle(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::{base_qwen_edit_2511_angles_image_request, get_artcraft_client};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_angle_edit_qwen_edit_2511() {
|
||||
let client = get_artcraft_client();
|
||||
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::SquareHd),
|
||||
image_batch_count: Some(1),
|
||||
prompt: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
horizontal_angle: Some(30.0),
|
||||
vertical_angle: Some(0.0),
|
||||
zoom: Some(1.0),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
|
||||
println!("Job token: {:?}", payload.inference_job_token);
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
}
|
||||
-2
@@ -9,5 +9,3 @@ pub mod generate_image_artcraft_nano_banana_pro;
|
||||
pub mod generate_image_artcraft_seedream_4;
|
||||
pub mod generate_image_artcraft_seedream_4p5;
|
||||
pub mod generate_image_artcraft_seedream_5_lite;
|
||||
pub mod generate_image_artcraft_qwen_edit_2511_angles;
|
||||
pub mod generate_image_artcraft_flux_2_lora_angles;
|
||||
|
||||
+1
-20
@@ -19,8 +19,6 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::plan_generate_image_artcraft_seedream_4;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::plan_generate_image_artcraft_seedream_4p5;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::plan_generate_image_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::plan_generate_image_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::plan_generate_image_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::plan_generate_image_fal_nano_banana_pro;
|
||||
|
||||
pub struct GenerateImageRequest<'a> {
|
||||
@@ -47,15 +45,6 @@ pub struct GenerateImageRequest<'a> {
|
||||
/// How many images to generate.
|
||||
pub image_batch_count: Option<u16>,
|
||||
|
||||
/// Only for angle manipulation models.
|
||||
pub horizontal_angle: Option<f64>,
|
||||
|
||||
/// Only for angle manipulation models.
|
||||
pub vertical_angle: Option<f64>,
|
||||
|
||||
/// Only for angle manipulation models.
|
||||
pub zoom: Option<f64>,
|
||||
|
||||
/// If the request is a mismatch with the (model/provider), how to mitigate it.
|
||||
pub request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy,
|
||||
|
||||
@@ -107,12 +96,6 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
CommonImageModel::Seedream5Lite => {
|
||||
plan_generate_image_artcraft_seedream_5_lite(self).map(ImageGenerationPlan::ArtcraftSeedream5Lite)
|
||||
}
|
||||
CommonImageModel::QwenEdit2511Angles => {
|
||||
plan_generate_image_artcraft_qwen_edit_2511_angles(self).map(ImageGenerationPlan::ArtcraftQwenEdit2511Angles)
|
||||
}
|
||||
CommonImageModel::Flux2LoraAngles => {
|
||||
plan_generate_image_artcraft_flux_2_lora_angles(self).map(ImageGenerationPlan::ArtcraftFlux2LoraAngles)
|
||||
}
|
||||
},
|
||||
Provider::Fal => match self.model {
|
||||
CommonImageModel::NanaBananaPro => {
|
||||
@@ -127,9 +110,7 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
| CommonImageModel::NanaBanana2
|
||||
| CommonImageModel::Seedream4
|
||||
| CommonImageModel::Seedream4p5
|
||||
| CommonImageModel::Seedream5Lite
|
||||
| CommonImageModel::QwenEdit2511Angles
|
||||
| CommonImageModel::Flux2LoraAngles => {
|
||||
| CommonImageModel::Seedream5Lite => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "provider",
|
||||
value: format!("{:?} is only available on the Artcraft provider", self.model),
|
||||
|
||||
-22
@@ -11,8 +11,6 @@ use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraf
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4::estimate_image_cost_artcraft_seedream_4;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4p5::estimate_image_cost_artcraft_seedream_4p5;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_5_lite::estimate_image_cost_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_qwen_edit_2511_angles::estimate_image_cost_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_2_lora_angles::estimate_image_cost_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_pro::estimate_image_cost_fal_nano_banana_pro;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_dev::execute_artcraft_flux_1_dev;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_schnell::execute_artcraft_flux_1_schnell;
|
||||
@@ -25,8 +23,6 @@ use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4::execute_artcraft_seedream_4;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4p5::execute_artcraft_seedream_4p5;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_5_lite::execute_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_qwen_edit_2511_angles::execute_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_2_lora_angles::execute_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_pro::execute_fal_nano_banana_pro;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
@@ -41,8 +37,6 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::PlanArtcraftSeedream4;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::PlanArtcraftSeedream4p5;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::PlanFalNanaBananaPro;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -58,8 +52,6 @@ pub enum ImageGenerationPlan<'a> {
|
||||
ArtcraftSeedream4(PlanArtcraftSeedream4<'a>),
|
||||
ArtcraftSeedream4p5(PlanArtcraftSeedream4p5<'a>),
|
||||
ArtcraftSeedream5Lite(PlanArtcraftSeedream5Lite<'a>),
|
||||
ArtcraftQwenEdit2511Angles(PlanArtcraftQwenEdit2511Angles<'a>),
|
||||
ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles<'a>),
|
||||
FalNanaBananaPro(PlanFalNanaBananaPro<'a>),
|
||||
}
|
||||
|
||||
@@ -113,14 +105,6 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_seedream_5_lite(plan, artcraft_client).await
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_qwen_edit_2511_angles(plan, artcraft_client).await
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_flux_2_lora_angles(plan, artcraft_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalNanaBananaPro(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_nano_banana_pro(plan, fal_client).await
|
||||
@@ -163,12 +147,6 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
ImageGenerationPlan::ArtcraftSeedream5Lite(plan) => {
|
||||
estimate_image_cost_artcraft_seedream_5_lite(plan)
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) => {
|
||||
estimate_image_cost_artcraft_qwen_edit_2511_angles(plan)
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) => {
|
||||
estimate_image_cost_artcraft_flux_2_lora_angles(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalNanaBananaPro(plan) => {
|
||||
estimate_image_cost_fal_nano_banana_pro(plan)
|
||||
}
|
||||
|
||||
-2
@@ -9,5 +9,3 @@ pub mod plan_generate_image_artcraft_nano_banana_pro;
|
||||
pub mod plan_generate_image_artcraft_seedream_4;
|
||||
pub mod plan_generate_image_artcraft_seedream_4p5;
|
||||
pub mod plan_generate_image_artcraft_seedream_5_lite;
|
||||
pub mod plan_generate_image_artcraft_qwen_edit_2511_angles;
|
||||
pub mod plan_generate_image_artcraft_flux_2_lora_angles;
|
||||
|
||||
-254
@@ -1,254 +0,0 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
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_image::generate_image_request::GenerateImageRequest;
|
||||
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
|
||||
Flux2LoraEditImageAngleImageSize,
|
||||
Flux2LoraEditImageAngleNumImages,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftFlux2LoraAngles<'a> {
|
||||
pub image_input: &'a MediaFileToken,
|
||||
pub horizontal_angle: Option<f64>,
|
||||
pub vertical_angle: Option<f64>,
|
||||
pub zoom: Option<f64>,
|
||||
pub image_size: Option<Flux2LoraEditImageAngleImageSize>,
|
||||
pub num_images: Flux2LoraEditImageAngleNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_flux_2_lora_angles<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftFlux2LoraAngles<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Angle models require exactly one input image.
|
||||
let image_input = resolve_single_image_input(request.image_inputs)?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftFlux2LoraAngles {
|
||||
image_input,
|
||||
horizontal_angle: request.horizontal_angle,
|
||||
vertical_angle: request.vertical_angle,
|
||||
zoom: request.zoom,
|
||||
image_size,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_single_image_input<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<&'a MediaFileToken, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: "Angle models require exactly one input image".to_string(),
|
||||
}))
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => {
|
||||
if tokens.len() != 1 {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Angle models require exactly one input image, got {}", tokens.len()),
|
||||
}));
|
||||
}
|
||||
Ok(&tokens[0])
|
||||
}
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flux 2 LoRA supported image sizes:
|
||||
// Square, SquareHd
|
||||
// PortraitFourThree, PortraitSixteenNine
|
||||
// LandscapeFourThree, LandscapeSixteenNine
|
||||
fn plan_image_size(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Flux2LoraEditImageAngleImageSize>, ArtcraftRouterError> {
|
||||
use Flux2LoraEditImageAngleImageSize as S;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto4k) => {
|
||||
Ok(Some(S::SquareHd))
|
||||
}
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
|
||||
|
||||
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
|
||||
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(unsupported @ CommonAspectRatio::TallFourByFive)
|
||||
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
|
||||
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::SquareHd)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Flux2LoraEditImageAngleNumImages, ArtcraftRouterError> {
|
||||
use Flux2LoraEditImageAngleNumImages as N;
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(N::One),
|
||||
2 => Ok(N::Two),
|
||||
3 => Ok(N::Three),
|
||||
4 => Ok(N::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(N::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_flux_2_lora_angles_image_request;
|
||||
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
|
||||
Flux2LoraEditImageAngleImageSize as S,
|
||||
Flux2LoraEditImageAngleNumImages as N,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn make_token() -> MediaFileToken {
|
||||
MediaFileToken::new_from_str("test_token_123")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_image_input() {
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: None,
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_urls() {
|
||||
let urls = vec!["https://example.com/img.png".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_single_media_token() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_media_tokens() {
|
||||
let tokens = vec![make_token(), make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_none_is_none() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
|
||||
assert!(plan.image_size.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_square_maps_directly() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
|
||||
assert!(matches!(plan.image_size, Some(S::Square)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_direct_mapping() {
|
||||
let tokens = vec![make_token()];
|
||||
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
|
||||
for (count, expected) in cases {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(count),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
|
||||
assert!(
|
||||
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
|
||||
"expected {:?} for count {}", expected, count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn angle_fields_are_passed_through() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
horizontal_angle: Some(45.0),
|
||||
vertical_angle: Some(-30.0),
|
||||
zoom: Some(1.5),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
|
||||
assert_eq!(plan.horizontal_angle, Some(45.0));
|
||||
assert_eq!(plan.vertical_angle, Some(-30.0));
|
||||
assert_eq!(plan.zoom, Some(1.5));
|
||||
}
|
||||
}
|
||||
-256
@@ -1,256 +0,0 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
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_image::generate_image_request::GenerateImageRequest;
|
||||
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::{
|
||||
QwenEdit2511EditImageAngleImageSize,
|
||||
QwenEdit2511EditImageAngleNumImages,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftQwenEdit2511Angles<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub image_input: &'a MediaFileToken,
|
||||
pub horizontal_angle: Option<f64>,
|
||||
pub vertical_angle: Option<f64>,
|
||||
pub zoom: Option<f64>,
|
||||
pub image_size: Option<QwenEdit2511EditImageAngleImageSize>,
|
||||
pub num_images: QwenEdit2511EditImageAngleNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_qwen_edit_2511_angles<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftQwenEdit2511Angles<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Angle models require exactly one input image.
|
||||
let image_input = resolve_single_image_input(request.image_inputs)?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftQwenEdit2511Angles {
|
||||
prompt: request.prompt,
|
||||
image_input,
|
||||
horizontal_angle: request.horizontal_angle,
|
||||
vertical_angle: request.vertical_angle,
|
||||
zoom: request.zoom,
|
||||
image_size,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_single_image_input<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<&'a MediaFileToken, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: "Angle models require exactly one input image".to_string(),
|
||||
}))
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => {
|
||||
if tokens.len() != 1 {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Angle models require exactly one input image, got {}", tokens.len()),
|
||||
}));
|
||||
}
|
||||
Ok(&tokens[0])
|
||||
}
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Qwen Edit 2511 supported image sizes:
|
||||
// Square, SquareHd
|
||||
// PortraitFourThree, PortraitSixteenNine
|
||||
// LandscapeFourThree, LandscapeSixteenNine
|
||||
fn plan_image_size(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<QwenEdit2511EditImageAngleImageSize>, ArtcraftRouterError> {
|
||||
use QwenEdit2511EditImageAngleImageSize as S;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto4k) => {
|
||||
Ok(Some(S::SquareHd))
|
||||
}
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
|
||||
|
||||
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
|
||||
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(unsupported @ CommonAspectRatio::TallFourByFive)
|
||||
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
|
||||
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::SquareHd)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<QwenEdit2511EditImageAngleNumImages, ArtcraftRouterError> {
|
||||
use QwenEdit2511EditImageAngleNumImages as N;
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(N::One),
|
||||
2 => Ok(N::Two),
|
||||
3 => Ok(N::Three),
|
||||
4 => Ok(N::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(N::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_qwen_edit_2511_angles_image_request;
|
||||
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::{
|
||||
QwenEdit2511EditImageAngleImageSize as S,
|
||||
QwenEdit2511EditImageAngleNumImages as N,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn make_token() -> MediaFileToken {
|
||||
MediaFileToken::new_from_str("test_token_123")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_image_input() {
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: None,
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_urls() {
|
||||
let urls = vec!["https://example.com/img.png".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_single_media_token() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_media_tokens() {
|
||||
let tokens = vec![make_token(), make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_none_is_none() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
|
||||
assert!(plan.image_size.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_square_maps_directly() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
|
||||
assert!(matches!(plan.image_size, Some(S::Square)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_direct_mapping() {
|
||||
let tokens = vec![make_token()];
|
||||
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
|
||||
for (count, expected) in cases {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(count),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
|
||||
assert!(
|
||||
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
|
||||
"expected {:?} for count {}", expected, count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn angle_fields_are_passed_through() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
horizontal_angle: Some(45.0),
|
||||
vertical_angle: Some(-30.0),
|
||||
zoom: Some(1.5),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
|
||||
assert_eq!(plan.horizontal_angle, Some(45.0));
|
||||
assert_eq!(plan.vertical_angle, Some(-30.0));
|
||||
assert_eq!(plan.zoom, Some(1.5));
|
||||
}
|
||||
}
|
||||
-3
@@ -254,9 +254,6 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_mini::PlanArtcraftMarble0p1Mini;
|
||||
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
|
||||
use world_labs_api::api::api_types::world_labs_model::WorldLabsModel;
|
||||
use world_labs_api::pricing::check_pricing::{calculate_cost, InputType};
|
||||
|
||||
pub(crate) fn estimate_splat_cost_artcraft_marble_0p1_mini(
|
||||
plan: &PlanArtcraftMarble0p1Mini<'_>,
|
||||
) -> SplatGenerationCostEstimate {
|
||||
let input_type = if plan.reference_image.is_some() {
|
||||
InputType::ImageNonPanorama
|
||||
} else {
|
||||
InputType::Text
|
||||
};
|
||||
|
||||
let cost = calculate_cost(WorldLabsModel::Marble0p1Mini, input_type);
|
||||
let cost_in_usd_cents = cost.us_dollar_cents as u64;
|
||||
|
||||
SplatGenerationCostEstimate {
|
||||
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_splat_model::CommonSplatModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn estimate_usd_cents(prompt: Option<&str>, image_tokens: Option<&Vec<MediaFileToken>>) -> u64 {
|
||||
let request = GenerateSplatRequest {
|
||||
model: CommonSplatModel::Marble0p1Mini,
|
||||
provider: Provider::Artcraft,
|
||||
prompt,
|
||||
reference_images: image_tokens.map(crate::api::image_list_ref::ImageListRef::MediaFileTokens),
|
||||
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 test_estimate_cost_text_only() {
|
||||
let cost = estimate_usd_cents(Some("a room"), None);
|
||||
assert_eq!(cost, 18); // 230 credits → 18 cents
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_no_image_no_prompt() {
|
||||
let cost = estimate_usd_cents(None, None);
|
||||
assert_eq!(cost, 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_image() {
|
||||
let tokens = vec![MediaFileToken("test_token".to_string())];
|
||||
let cost = estimate_usd_cents(None, Some(&tokens));
|
||||
assert_eq!(cost, 18); // 230 credits → 18 cents (same as text for mini)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_image_with_prompt() {
|
||||
let tokens = vec![MediaFileToken("test_token".to_string())];
|
||||
let cost = estimate_usd_cents(Some("a cozy room"), Some(&tokens));
|
||||
assert_eq!(cost, 18); // 230 credits → 18 cents
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_plus::PlanArtcraftMarble0p1Plus;
|
||||
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
|
||||
use world_labs_api::api::api_types::world_labs_model::WorldLabsModel;
|
||||
use world_labs_api::pricing::check_pricing::{calculate_cost, InputType};
|
||||
|
||||
pub(crate) fn estimate_splat_cost_artcraft_marble_0p1_plus(
|
||||
plan: &PlanArtcraftMarble0p1Plus<'_>,
|
||||
) -> SplatGenerationCostEstimate {
|
||||
let input_type = if plan.reference_image.is_some() {
|
||||
InputType::ImageNonPanorama
|
||||
} else {
|
||||
InputType::Text
|
||||
};
|
||||
|
||||
let cost = calculate_cost(WorldLabsModel::Marble0p1Plus, input_type);
|
||||
let cost_in_usd_cents = cost.us_dollar_cents as u64;
|
||||
|
||||
SplatGenerationCostEstimate {
|
||||
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_splat_model::CommonSplatModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn estimate_usd_cents(prompt: Option<&str>, image_tokens: Option<&Vec<MediaFileToken>>) -> u64 {
|
||||
let request = GenerateSplatRequest {
|
||||
model: CommonSplatModel::Marble0p1Plus,
|
||||
provider: Provider::Artcraft,
|
||||
prompt,
|
||||
reference_images: image_tokens.map(crate::api::image_list_ref::ImageListRef::MediaFileTokens),
|
||||
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 test_estimate_cost_text_only() {
|
||||
let cost = estimate_usd_cents(Some("a room"), None);
|
||||
assert_eq!(cost, 126); // 1580 credits → 126 cents
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_no_image_no_prompt() {
|
||||
let cost = estimate_usd_cents(None, None);
|
||||
assert_eq!(cost, 126);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_image() {
|
||||
let tokens = vec![MediaFileToken("test_token".to_string())];
|
||||
let cost = estimate_usd_cents(None, Some(&tokens));
|
||||
assert_eq!(cost, 126); // 1580 credits → 126 cents (same as text for plus)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_image_with_prompt() {
|
||||
let tokens = vec![MediaFileToken("test_token".to_string())];
|
||||
let cost = estimate_usd_cents(Some("a cozy room"), Some(&tokens));
|
||||
assert_eq!(cost, 126); // 1580 credits → 126 cents
|
||||
}
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
pub mod estimate_splat_cost_artcraft_marble_0p1_mini;
|
||||
pub mod estimate_splat_cost_artcraft_marble_0p1_plus;
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod artcraft;
|
||||
-32
@@ -1,32 +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_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_mini::PlanArtcraftMarble0p1Mini;
|
||||
use crate::generate::generate_splat::generate_splat_response::{
|
||||
ArtcraftSplatResponsePayload, GenerateSplatResponse,
|
||||
};
|
||||
use artcraft_api_defs::generate::splat::generate_worldlabs_marble_0p1_mini_splat::GenerateWorldlabsMarble0p1MiniSplatRequest;
|
||||
use artcraft_client::endpoints::generate::splat::generate_worldlabs_marble_0p1_mini_splat::generate_worldlabs_marble_0p1_mini_splat;
|
||||
|
||||
pub async fn execute_artcraft_marble_0p1_mini(
|
||||
plan: &PlanArtcraftMarble0p1Mini<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
|
||||
let request = GenerateWorldlabsMarble0p1MiniSplatRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
image_media_file_token: plan.reference_image.cloned(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
};
|
||||
|
||||
let response = generate_worldlabs_marble_0p1_mini_splat(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateSplatResponse::Artcraft(ArtcraftSplatResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
}))
|
||||
}
|
||||
-32
@@ -1,32 +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_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_plus::PlanArtcraftMarble0p1Plus;
|
||||
use crate::generate::generate_splat::generate_splat_response::{
|
||||
ArtcraftSplatResponsePayload, GenerateSplatResponse,
|
||||
};
|
||||
use artcraft_api_defs::generate::splat::generate_worldlabs_marble_0p1_plus_splat::GenerateWorldlabsMarble0p1PlusSplatRequest;
|
||||
use artcraft_client::endpoints::generate::splat::generate_worldlabs_marble_0p1_plus_splat::generate_worldlabs_marble_0p1_plus_splat;
|
||||
|
||||
pub async fn execute_artcraft_marble_0p1_plus(
|
||||
plan: &PlanArtcraftMarble0p1Plus<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
|
||||
let request = GenerateWorldlabsMarble0p1PlusSplatRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
image_media_file_token: plan.reference_image.cloned(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
};
|
||||
|
||||
let response = generate_worldlabs_marble_0p1_plus_splat(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateSplatResponse::Artcraft(ArtcraftSplatResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
}))
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
pub mod generate_splat_artcraft_marble_0p1_mini;
|
||||
pub mod generate_splat_artcraft_marble_0p1_plus;
|
||||
@@ -1 +0,0 @@
|
||||
pub mod artcraft;
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
use crate::api::common_splat_model::CommonSplatModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_mini::plan_generate_splat_artcraft_marble_0p1_mini;
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_plus::plan_generate_splat_artcraft_marble_0p1_plus;
|
||||
use crate::generate::generate_splat::splat_generation_plan::SplatGenerationPlan;
|
||||
|
||||
pub struct GenerateSplatRequest<'a> {
|
||||
/// Which model to use.
|
||||
pub model: CommonSplatModel,
|
||||
|
||||
/// Which provider to use.
|
||||
pub provider: Provider,
|
||||
|
||||
/// The prompt for splat generation (optional).
|
||||
pub prompt: Option<&'a str>,
|
||||
|
||||
/// Reference images (optional).
|
||||
pub reference_images: Option<ImageListRef<'a>>,
|
||||
|
||||
/// Some providers support idempotency.
|
||||
/// If not supplied, we'll generate one for the required providers.
|
||||
pub idempotency_token: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> GenerateSplatRequest<'a> {
|
||||
/// Read the splat generation request, construct a plan, then yield a means to execute it.
|
||||
pub fn build(&self) -> Result<SplatGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.provider {
|
||||
Provider::Artcraft => match self.model {
|
||||
CommonSplatModel::Marble0p1Mini => {
|
||||
plan_generate_splat_artcraft_marble_0p1_mini(self).map(SplatGenerationPlan::ArtcraftMarble0p1Mini)
|
||||
}
|
||||
CommonSplatModel::Marble0p1Plus => {
|
||||
plan_generate_splat_artcraft_marble_0p1_plus(self).map(SplatGenerationPlan::ArtcraftMarble0p1Plus)
|
||||
}
|
||||
},
|
||||
Provider::Fal => Err(ArtcraftRouterError::UnsupportedModel(
|
||||
format!("Splat generation via Fal is not yet supported (model: {:?})", self.model)
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_generate_idempotency_token(&self) -> String {
|
||||
self.idempotency_token.map(|t| t.to_string())
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSplatResponsePayload {
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum GenerateSplatResponse {
|
||||
Artcraft(ArtcraftSplatResponsePayload),
|
||||
}
|
||||
|
||||
impl GenerateSplatResponse {
|
||||
pub fn get_artcraft_payload(&self) -> Option<ArtcraftSplatResponsePayload> {
|
||||
match self {
|
||||
Self::Artcraft(p) => Some(p.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod cost;
|
||||
pub mod execute;
|
||||
pub mod generate_splat_request;
|
||||
pub mod generate_splat_response;
|
||||
pub mod plan;
|
||||
pub mod splat_generation_cost_estimate;
|
||||
pub mod splat_generation_plan;
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
pub mod plan_generate_splat_artcraft_marble_0p1_mini;
|
||||
pub mod plan_generate_splat_artcraft_marble_0p1_plus;
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftMarble0p1Mini<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub reference_image: Option<&'a MediaFileToken>,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_splat_artcraft_marble_0p1_mini<'a>(
|
||||
request: &'a GenerateSplatRequest<'a>,
|
||||
) -> Result<PlanArtcraftMarble0p1Mini<'a>, ArtcraftRouterError> {
|
||||
let reference_image = resolve_single_image_ref(request.reference_images)?;
|
||||
|
||||
Ok(PlanArtcraftMarble0p1Mini {
|
||||
prompt: request.prompt,
|
||||
reference_image,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_single_image_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a MediaFileToken>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => {
|
||||
Ok(tokens.first())
|
||||
}
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftMarble0p1Plus<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub reference_image: Option<&'a MediaFileToken>,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_splat_artcraft_marble_0p1_plus<'a>(
|
||||
request: &'a GenerateSplatRequest<'a>,
|
||||
) -> Result<PlanArtcraftMarble0p1Plus<'a>, ArtcraftRouterError> {
|
||||
let reference_image = resolve_single_image_ref(request.reference_images)?;
|
||||
|
||||
Ok(PlanArtcraftMarble0p1Plus {
|
||||
prompt: request.prompt,
|
||||
reference_image,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_single_image_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a MediaFileToken>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => {
|
||||
Ok(tokens.first())
|
||||
}
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod artcraft;
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
/// The result of estimating the cost of a splat generation plan.
|
||||
pub struct SplatGenerationCostEstimate {
|
||||
pub cost_in_credits: Option<u64>,
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
pub is_free: bool,
|
||||
pub is_unlimited: bool,
|
||||
pub is_rate_limited: bool,
|
||||
pub has_watermark: bool,
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_splat::cost::artcraft::estimate_splat_cost_artcraft_marble_0p1_mini::estimate_splat_cost_artcraft_marble_0p1_mini;
|
||||
use crate::generate::generate_splat::cost::artcraft::estimate_splat_cost_artcraft_marble_0p1_plus::estimate_splat_cost_artcraft_marble_0p1_plus;
|
||||
use crate::generate::generate_splat::execute::artcraft::generate_splat_artcraft_marble_0p1_mini::execute_artcraft_marble_0p1_mini;
|
||||
use crate::generate::generate_splat::execute::artcraft::generate_splat_artcraft_marble_0p1_plus::execute_artcraft_marble_0p1_plus;
|
||||
use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse;
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_mini::PlanArtcraftMarble0p1Mini;
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_plus::PlanArtcraftMarble0p1Plus;
|
||||
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SplatGenerationPlan<'a> {
|
||||
ArtcraftMarble0p1Mini(PlanArtcraftMarble0p1Mini<'a>),
|
||||
ArtcraftMarble0p1Plus(PlanArtcraftMarble0p1Plus<'a>),
|
||||
}
|
||||
|
||||
impl<'a> SplatGenerationPlan<'a> {
|
||||
pub async fn generate_splat(
|
||||
&self,
|
||||
client: &RouterClient,
|
||||
) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
SplatGenerationPlan::ArtcraftMarble0p1Mini(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_marble_0p1_mini(plan, artcraft_client).await
|
||||
}
|
||||
SplatGenerationPlan::ArtcraftMarble0p1Plus(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_marble_0p1_plus(plan, artcraft_client).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_costs(&self) -> SplatGenerationCostEstimate {
|
||||
match self {
|
||||
SplatGenerationPlan::ArtcraftMarble0p1Mini(plan) => {
|
||||
estimate_splat_cost_artcraft_marble_0p1_mini(plan)
|
||||
}
|
||||
SplatGenerationPlan::ArtcraftMarble0p1Plus(plan) => {
|
||||
estimate_splat_cost_artcraft_marble_0p1_plus(plan)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::Kling3p0ProMultiFunctionVideoGenDuration;
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::webhook::video::text::enqueue_kling_3p0_pro_text_to_video_webhook::{
|
||||
EnqueueKling3p0ProTextToVideoArgs,
|
||||
EnqueueKling3p0ProTextToVideoDuration,
|
||||
};
|
||||
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_pro::PlanArtcraftKling3p0Pro;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
pub(crate) fn estimate_video_cost_artcraft_kling3p0_pro(
|
||||
plan: &PlanArtcraftKling3p0Pro<'_>,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
let duration = plan.duration
|
||||
.map(|d| match d {
|
||||
Kling3p0ProMultiFunctionVideoGenDuration::ThreeSeconds => EnqueueKling3p0ProTextToVideoDuration::ThreeSeconds,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration::FourSeconds => EnqueueKling3p0ProTextToVideoDuration::FourSeconds,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration::FiveSeconds => EnqueueKling3p0ProTextToVideoDuration::FiveSeconds,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration::SixSeconds => EnqueueKling3p0ProTextToVideoDuration::SixSeconds,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration::SevenSeconds => EnqueueKling3p0ProTextToVideoDuration::SevenSeconds,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration::EightSeconds => EnqueueKling3p0ProTextToVideoDuration::EightSeconds,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration::NineSeconds => EnqueueKling3p0ProTextToVideoDuration::NineSeconds,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration::TenSeconds => EnqueueKling3p0ProTextToVideoDuration::TenSeconds,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration::ElevenSeconds => EnqueueKling3p0ProTextToVideoDuration::ElevenSeconds,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration::TwelveSeconds => EnqueueKling3p0ProTextToVideoDuration::TwelveSeconds,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration::ThirteenSeconds => EnqueueKling3p0ProTextToVideoDuration::ThirteenSeconds,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration::FourteenSeconds => EnqueueKling3p0ProTextToVideoDuration::FourteenSeconds,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration::FifteenSeconds => EnqueueKling3p0ProTextToVideoDuration::FifteenSeconds,
|
||||
});
|
||||
|
||||
// Use text-to-video cost calculator (same pricing for both modes)
|
||||
let api_key = fal_client::creds::fal_api_key::FalApiKey::from_str("");
|
||||
let args = EnqueueKling3p0ProTextToVideoArgs {
|
||||
prompt: String::new(),
|
||||
generate_audio: plan.generate_audio,
|
||||
negative_prompt: None,
|
||||
duration,
|
||||
aspect_ratio: None,
|
||||
shot_type: None,
|
||||
webhook_url: "https://example.com",
|
||||
api_key: &api_key,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = args.calculate_cost_in_cents();
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[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) -> u64 {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Kling3p0Pro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: None,
|
||||
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 test_estimate_cost_usd_cents() {
|
||||
// Kling 3.0 Pro: $0.336/sec (audio on, the default when None)
|
||||
// Formula: (336 * duration_secs + 9) / 10 → ceiling division
|
||||
// 5s: (336 * 5 + 9) / 10 = 1689 / 10 = 168
|
||||
assert_eq!(estimate_usd_cents(5), 168);
|
||||
// 10s: (336 * 10 + 9) / 10 = 3369 / 10 = 336
|
||||
assert_eq!(estimate_usd_cents(10), 336);
|
||||
// 15s: (336 * 15 + 9) / 10 = 5049 / 10 = 504
|
||||
assert_eq!(estimate_usd_cents(15), 504);
|
||||
}
|
||||
}
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::Kling3p0StandardMultiFunctionVideoGenDuration;
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::webhook::video::text::enqueue_kling_3p0_standard_text_to_video_webhook::{
|
||||
EnqueueKling3p0StandardTextToVideoArgs,
|
||||
EnqueueKling3p0StandardTextToVideoDuration,
|
||||
};
|
||||
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::PlanArtcraftKling3p0Standard;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
pub(crate) fn estimate_video_cost_artcraft_kling3p0_standard(
|
||||
plan: &PlanArtcraftKling3p0Standard<'_>,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
let duration = plan.duration
|
||||
.map(|d| match d {
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration::ThreeSeconds => EnqueueKling3p0StandardTextToVideoDuration::ThreeSeconds,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration::FourSeconds => EnqueueKling3p0StandardTextToVideoDuration::FourSeconds,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration::FiveSeconds => EnqueueKling3p0StandardTextToVideoDuration::FiveSeconds,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration::SixSeconds => EnqueueKling3p0StandardTextToVideoDuration::SixSeconds,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration::SevenSeconds => EnqueueKling3p0StandardTextToVideoDuration::SevenSeconds,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration::EightSeconds => EnqueueKling3p0StandardTextToVideoDuration::EightSeconds,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration::NineSeconds => EnqueueKling3p0StandardTextToVideoDuration::NineSeconds,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration::TenSeconds => EnqueueKling3p0StandardTextToVideoDuration::TenSeconds,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration::ElevenSeconds => EnqueueKling3p0StandardTextToVideoDuration::ElevenSeconds,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration::TwelveSeconds => EnqueueKling3p0StandardTextToVideoDuration::TwelveSeconds,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration::ThirteenSeconds => EnqueueKling3p0StandardTextToVideoDuration::ThirteenSeconds,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration::FourteenSeconds => EnqueueKling3p0StandardTextToVideoDuration::FourteenSeconds,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration::FifteenSeconds => EnqueueKling3p0StandardTextToVideoDuration::FifteenSeconds,
|
||||
});
|
||||
|
||||
// Use text-to-video cost calculator (same pricing for both modes)
|
||||
let api_key = fal_client::creds::fal_api_key::FalApiKey::from_str("");
|
||||
let args = EnqueueKling3p0StandardTextToVideoArgs {
|
||||
prompt: String::new(),
|
||||
generate_audio: plan.generate_audio,
|
||||
negative_prompt: None,
|
||||
duration,
|
||||
aspect_ratio: None,
|
||||
shot_type: None,
|
||||
webhook_url: "https://example.com",
|
||||
api_key: &api_key,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = args.calculate_cost_in_cents();
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[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) -> u64 {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Kling3p0Standard,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: None,
|
||||
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 test_estimate_cost_usd_cents() {
|
||||
// Kling 3.0 Standard: $0.252/sec (audio on, the default when None)
|
||||
// Formula: (252 * duration_secs + 9) / 10 → ceiling division
|
||||
// 5s: (252 * 5 + 9) / 10 = 1269 / 10 = 126
|
||||
assert_eq!(estimate_usd_cents(5), 126);
|
||||
// 10s: (252 * 10 + 9) / 10 = 2529 / 10 = 252
|
||||
assert_eq!(estimate_usd_cents(10), 252);
|
||||
// 15s: (252 * 15 + 9) / 10 = 3789 / 10 = 378
|
||||
assert_eq!(estimate_usd_cents(15), 378);
|
||||
}
|
||||
}
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::Seedance1p5ProMultiFunctionVideoGenDuration;
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::webhook::video::text::enqueue_seedance_1p5_pro_text_to_video_webhook::{
|
||||
EnqueueSeedance1p5ProTextToVideoArgs,
|
||||
EnqueueSeedance1p5ProTextToVideoDuration,
|
||||
EnqueueSeedance1p5ProTextToVideoResolution,
|
||||
};
|
||||
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance1p5_pro::PlanArtcraftSeedance1p5Pro;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
pub(crate) fn estimate_video_cost_artcraft_seedance1p5_pro(
|
||||
plan: &PlanArtcraftSeedance1p5Pro<'_>,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
let duration = plan.duration
|
||||
.map(|d| match d {
|
||||
Seedance1p5ProMultiFunctionVideoGenDuration::FourSeconds => EnqueueSeedance1p5ProTextToVideoDuration::FourSeconds,
|
||||
Seedance1p5ProMultiFunctionVideoGenDuration::FiveSeconds => EnqueueSeedance1p5ProTextToVideoDuration::FiveSeconds,
|
||||
Seedance1p5ProMultiFunctionVideoGenDuration::SixSeconds => EnqueueSeedance1p5ProTextToVideoDuration::SixSeconds,
|
||||
Seedance1p5ProMultiFunctionVideoGenDuration::SevenSeconds => EnqueueSeedance1p5ProTextToVideoDuration::SevenSeconds,
|
||||
Seedance1p5ProMultiFunctionVideoGenDuration::EightSeconds => EnqueueSeedance1p5ProTextToVideoDuration::EightSeconds,
|
||||
Seedance1p5ProMultiFunctionVideoGenDuration::NineSeconds => EnqueueSeedance1p5ProTextToVideoDuration::NineSeconds,
|
||||
Seedance1p5ProMultiFunctionVideoGenDuration::TenSeconds => EnqueueSeedance1p5ProTextToVideoDuration::TenSeconds,
|
||||
Seedance1p5ProMultiFunctionVideoGenDuration::ElevenSeconds => EnqueueSeedance1p5ProTextToVideoDuration::ElevenSeconds,
|
||||
Seedance1p5ProMultiFunctionVideoGenDuration::TwelveSeconds => EnqueueSeedance1p5ProTextToVideoDuration::TwelveSeconds,
|
||||
});
|
||||
|
||||
let resolution = plan.resolution
|
||||
.map(|r| match r {
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::Seedance1p5ProMultiFunctionVideoGenResolution::FourEightyP => EnqueueSeedance1p5ProTextToVideoResolution::FourEightyP,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::Seedance1p5ProMultiFunctionVideoGenResolution::SevenTwentyP => EnqueueSeedance1p5ProTextToVideoResolution::SevenTwentyP,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::Seedance1p5ProMultiFunctionVideoGenResolution::TenEightyP => EnqueueSeedance1p5ProTextToVideoResolution::TenEightyP,
|
||||
});
|
||||
|
||||
// Use text-to-video cost calculator (same pricing for both modes)
|
||||
let api_key = fal_client::creds::fal_api_key::FalApiKey::from_str("");
|
||||
let args = EnqueueSeedance1p5ProTextToVideoArgs {
|
||||
prompt: String::new(),
|
||||
duration,
|
||||
resolution,
|
||||
aspect_ratio: None,
|
||||
generate_audio: plan.generate_audio,
|
||||
webhook_url: "https://example.com",
|
||||
api_key: &api_key,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = args.calculate_cost_in_cents();
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[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) -> u64 {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance1p5Pro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: None,
|
||||
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 test_estimate_cost_usd_cents() {
|
||||
// Default resolution is 720p (when None)
|
||||
// 720p 5s = 26 cents (constant from Fal)
|
||||
assert_eq!(estimate_usd_cents(5), 26);
|
||||
|
||||
// Other durations use token-based calculation
|
||||
assert_eq!(estimate_usd_cents(4), 26);
|
||||
assert_eq!(estimate_usd_cents(10), 65);
|
||||
assert_eq!(estimate_usd_cents(12), 78);
|
||||
}
|
||||
}
|
||||
-2
@@ -57,7 +57,6 @@ mod tests {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
@@ -65,7 +64,6 @@ mod tests {
|
||||
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,
|
||||
};
|
||||
|
||||
-3
@@ -1,4 +1 @@
|
||||
pub mod estimate_video_cost_artcraft_kling3p0_pro;
|
||||
pub mod estimate_video_cost_artcraft_kling3p0_standard;
|
||||
pub mod estimate_video_cost_artcraft_seedance1p5_pro;
|
||||
pub mod estimate_video_cost_artcraft_seedance2p0;
|
||||
|
||||
-39
@@ -1,39 +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::plan::artcraft::plan_generate_video_artcraft_kling3p0_pro::PlanArtcraftKling3p0Pro;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
ArtcraftVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::Kling3p0ProMultiFunctionVideoGenRequest;
|
||||
use artcraft_client::endpoints::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::kling_3p0_pro_multi_function_video_gen;
|
||||
|
||||
pub async fn execute_artcraft_kling3p0_pro(
|
||||
plan: &PlanArtcraftKling3p0Pro<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let request = Kling3p0ProMultiFunctionVideoGenRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
negative_prompt: plan.negative_prompt.map(|s| s.to_string()),
|
||||
image_media_token: plan.start_frame.map(|t| t.to_owned()),
|
||||
end_image_media_token: plan.end_frame.map(|t| t.to_owned()),
|
||||
duration: plan.duration,
|
||||
generate_audio: plan.generate_audio,
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
};
|
||||
|
||||
let response = kling_3p0_pro_multi_function_video_gen(
|
||||
&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,
|
||||
}))
|
||||
}
|
||||
-39
@@ -1,39 +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::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::PlanArtcraftKling3p0Standard;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
ArtcraftVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::Kling3p0StandardMultiFunctionVideoGenRequest;
|
||||
use artcraft_client::endpoints::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::kling_3p0_standard_multi_function_video_gen;
|
||||
|
||||
pub async fn execute_artcraft_kling3p0_standard(
|
||||
plan: &PlanArtcraftKling3p0Standard<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let request = Kling3p0StandardMultiFunctionVideoGenRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
negative_prompt: plan.negative_prompt.map(|s| s.to_string()),
|
||||
image_media_token: plan.start_frame.map(|t| t.to_owned()),
|
||||
end_image_media_token: plan.end_frame.map(|t| t.to_owned()),
|
||||
duration: plan.duration,
|
||||
generate_audio: plan.generate_audio,
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
};
|
||||
|
||||
let response = kling_3p0_standard_multi_function_video_gen(
|
||||
&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,
|
||||
}))
|
||||
}
|
||||
-39
@@ -1,39 +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::plan::artcraft::plan_generate_video_artcraft_seedance1p5_pro::PlanArtcraftSeedance1p5Pro;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
ArtcraftVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::Seedance1p5ProMultiFunctionVideoGenRequest;
|
||||
use artcraft_client::endpoints::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::seedance_1p5_pro_multi_function_video_gen;
|
||||
|
||||
pub async fn execute_artcraft_seedance1p5_pro(
|
||||
plan: &PlanArtcraftSeedance1p5Pro<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let request = Seedance1p5ProMultiFunctionVideoGenRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
start_frame_image_media_token: plan.start_frame.map(|t| t.to_owned()),
|
||||
end_frame_image_media_token: plan.end_frame.map(|t| t.to_owned()),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
duration: plan.duration,
|
||||
resolution: plan.resolution,
|
||||
generate_audio: plan.generate_audio,
|
||||
};
|
||||
|
||||
let response = seedance_1p5_pro_multi_function_video_gen(
|
||||
&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,
|
||||
}))
|
||||
}
|
||||
-3
@@ -1,4 +1 @@
|
||||
pub mod generate_video_artcraft_kling3p0_pro;
|
||||
pub mod generate_video_artcraft_kling3p0_standard;
|
||||
pub mod generate_video_artcraft_seedance1p5_pro;
|
||||
pub mod generate_video_artcraft_seedance2p0;
|
||||
|
||||
+5
-19
@@ -6,9 +6,6 @@ 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::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_pro::plan_generate_video_artcraft_kling3p0_pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::plan_generate_video_artcraft_kling3p0_standard;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance1p5_pro::plan_generate_video_artcraft_seedance1p5_pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::plan_generate_video_artcraft_seedance2p0;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
@@ -22,8 +19,8 @@ pub struct GenerateVideoRequest<'a> {
|
||||
/// The prompt for the video generation
|
||||
pub prompt: Option<&'a str>,
|
||||
|
||||
/// Some models support negative prompts
|
||||
pub negative_prompt: Option<&'a str>,
|
||||
// /// Some models support negative prompts
|
||||
// pub negative_prompt: Option<String>,
|
||||
|
||||
/// Starting keyframe (optional).
|
||||
pub start_frame: Option<ImageRef<'a>>,
|
||||
@@ -46,11 +43,9 @@ pub struct GenerateVideoRequest<'a> {
|
||||
/// How many videos to generate.
|
||||
pub video_batch_count: Option<u16>,
|
||||
|
||||
/// Whether to turn on/off audio.
|
||||
/// Not all models support audio, not all models have a choice.
|
||||
/// Some models will default this to true, others will default it to false,
|
||||
/// so it's best to be explicit.
|
||||
pub generate_audio: Option<bool>,
|
||||
// /// Whether to turn on/off audio.
|
||||
// /// Not all models support audio, not all models have a choice.
|
||||
// pub generate_audio: Option<bool>,
|
||||
|
||||
/// If the request is a mismatch with the (model/provider), how to mitigate it.
|
||||
pub request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy,
|
||||
@@ -65,15 +60,6 @@ impl<'a> GenerateVideoRequest<'a> {
|
||||
pub fn build(&self) -> Result<VideoGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.provider {
|
||||
Provider::Artcraft => match self.model {
|
||||
CommonVideoModel::Kling3p0Pro => {
|
||||
plan_generate_video_artcraft_kling3p0_pro(self).map(VideoGenerationPlan::ArtcraftKling3p0Pro)
|
||||
}
|
||||
CommonVideoModel::Kling3p0Standard => {
|
||||
plan_generate_video_artcraft_kling3p0_standard(self).map(VideoGenerationPlan::ArtcraftKling3p0Standard)
|
||||
}
|
||||
CommonVideoModel::Seedance1p5Pro => {
|
||||
plan_generate_video_artcraft_seedance1p5_pro(self).map(VideoGenerationPlan::ArtcraftSeedance1p5Pro)
|
||||
}
|
||||
CommonVideoModel::Seedance2p0 => {
|
||||
plan_generate_video_artcraft_seedance2p0(self).map(VideoGenerationPlan::ArtcraftSeedance2p0)
|
||||
}
|
||||
|
||||
-3
@@ -1,4 +1 @@
|
||||
pub mod plan_generate_video_artcraft_kling3p0_pro;
|
||||
pub mod plan_generate_video_artcraft_kling3p0_standard;
|
||||
pub mod plan_generate_video_artcraft_seedance1p5_pro;
|
||||
pub mod plan_generate_video_artcraft_seedance2p0;
|
||||
|
||||
-272
@@ -1,272 +0,0 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::{
|
||||
Kling3p0ProMultiFunctionVideoGenAspectRatio,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftKling3p0Pro<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub negative_prompt: Option<&'a str>,
|
||||
pub start_frame: Option<&'a MediaFileToken>,
|
||||
pub end_frame: Option<&'a MediaFileToken>,
|
||||
pub aspect_ratio: Option<Kling3p0ProMultiFunctionVideoGenAspectRatio>,
|
||||
pub duration: Option<Kling3p0ProMultiFunctionVideoGenDuration>,
|
||||
pub generate_audio: Option<bool>,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_kling3p0_pro<'a>(
|
||||
request: &'a GenerateVideoRequest<'a>,
|
||||
) -> Result<PlanArtcraftKling3p0Pro<'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 aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftKling3p0Pro {
|
||||
prompt: request.prompt,
|
||||
negative_prompt: request.negative_prompt,
|
||||
start_frame,
|
||||
end_frame,
|
||||
aspect_ratio,
|
||||
duration,
|
||||
generate_audio: request.generate_audio,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kling 3.0 Pro supports: Square (1:1), SixteenByNine (16:9), NineBySixteen (9:16)
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Kling3p0ProMultiFunctionVideoGenAspectRatio>, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
// Direct mappings
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::SixteenByNine))
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::NineBySixteen))
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::Square))
|
||||
}
|
||||
|
||||
// Auto defaults to 16:9
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => {
|
||||
Ok(Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::SixteenByNine))
|
||||
}
|
||||
|
||||
// Mismatches
|
||||
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) -> Kling3p0ProMultiFunctionVideoGenAspectRatio {
|
||||
match aspect_ratio {
|
||||
// Wide-ish → 16:9
|
||||
CommonAspectRatio::WideFourByThree
|
||||
| CommonAspectRatio::WideFiveByFour
|
||||
| CommonAspectRatio::WideThreeByTwo
|
||||
| CommonAspectRatio::WideTwentyOneByNine => Kling3p0ProMultiFunctionVideoGenAspectRatio::SixteenByNine,
|
||||
// Tall-ish → 9:16
|
||||
CommonAspectRatio::TallThreeByFour
|
||||
| CommonAspectRatio::TallFourByFive
|
||||
| CommonAspectRatio::TallTwoByThree
|
||||
| CommonAspectRatio::TallNineByTwentyOne => Kling3p0ProMultiFunctionVideoGenAspectRatio::NineBySixteen,
|
||||
// Fallback
|
||||
_ => Kling3p0ProMultiFunctionVideoGenAspectRatio::Square,
|
||||
}
|
||||
}
|
||||
|
||||
// Kling 3.0 Pro supports durations of 3–15 seconds.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Kling3p0ProMultiFunctionVideoGenDuration>, ArtcraftRouterError> {
|
||||
const MIN: u16 = 3;
|
||||
const MAX: u16 = 15;
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(Some(seconds_to_duration(d))),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(seconds_to_duration(d.clamp(MIN, MAX)))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn seconds_to_duration(seconds: u16) -> Kling3p0ProMultiFunctionVideoGenDuration {
|
||||
match seconds {
|
||||
3 => Kling3p0ProMultiFunctionVideoGenDuration::ThreeSeconds,
|
||||
4 => Kling3p0ProMultiFunctionVideoGenDuration::FourSeconds,
|
||||
5 => Kling3p0ProMultiFunctionVideoGenDuration::FiveSeconds,
|
||||
6 => Kling3p0ProMultiFunctionVideoGenDuration::SixSeconds,
|
||||
7 => Kling3p0ProMultiFunctionVideoGenDuration::SevenSeconds,
|
||||
8 => Kling3p0ProMultiFunctionVideoGenDuration::EightSeconds,
|
||||
9 => Kling3p0ProMultiFunctionVideoGenDuration::NineSeconds,
|
||||
10 => Kling3p0ProMultiFunctionVideoGenDuration::TenSeconds,
|
||||
11 => Kling3p0ProMultiFunctionVideoGenDuration::ElevenSeconds,
|
||||
12 => Kling3p0ProMultiFunctionVideoGenDuration::TwelveSeconds,
|
||||
13 => Kling3p0ProMultiFunctionVideoGenDuration::ThirteenSeconds,
|
||||
14 => Kling3p0ProMultiFunctionVideoGenDuration::FourteenSeconds,
|
||||
_ => Kling3p0ProMultiFunctionVideoGenDuration::FifteenSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
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::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;
|
||||
|
||||
fn base_request() -> GenerateVideoRequest<'static> {
|
||||
GenerateVideoRequest {
|
||||
model: CommonVideoModel::Kling3p0Pro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_16x9() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftKling3p0Pro(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
assert!(matches!(plan.aspect_ratio, Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::SixteenByNine)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_9x16() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftKling3p0Pro(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
assert!(matches!(plan.aspect_ratio, Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::NineBySixteen)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_square() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftKling3p0Pro(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
assert!(matches!(plan.aspect_ratio, Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::Square)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_error_out_on_unsupported() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_valid_range() {
|
||||
for d in 3..=15 {
|
||||
let request = GenerateVideoRequest {
|
||||
duration_seconds: Some(d),
|
||||
..base_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_ok(), "duration {} should be valid", d);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_out_of_range_error_out() {
|
||||
let request = GenerateVideoRequest {
|
||||
duration_seconds: Some(16),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_image_ref_returns_error() {
|
||||
let request = GenerateVideoRequest {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/image.jpg")),
|
||||
..base_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
}
|
||||
-272
@@ -1,272 +0,0 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::{
|
||||
Kling3p0StandardMultiFunctionVideoGenAspectRatio,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftKling3p0Standard<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub negative_prompt: Option<&'a str>,
|
||||
pub start_frame: Option<&'a MediaFileToken>,
|
||||
pub end_frame: Option<&'a MediaFileToken>,
|
||||
pub aspect_ratio: Option<Kling3p0StandardMultiFunctionVideoGenAspectRatio>,
|
||||
pub duration: Option<Kling3p0StandardMultiFunctionVideoGenDuration>,
|
||||
pub generate_audio: Option<bool>,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_kling3p0_standard<'a>(
|
||||
request: &'a GenerateVideoRequest<'a>,
|
||||
) -> Result<PlanArtcraftKling3p0Standard<'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 aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftKling3p0Standard {
|
||||
prompt: request.prompt,
|
||||
negative_prompt: request.negative_prompt,
|
||||
start_frame,
|
||||
end_frame,
|
||||
aspect_ratio,
|
||||
duration,
|
||||
generate_audio: request.generate_audio,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kling 3.0 Standard supports: Square (1:1), SixteenByNine (16:9), NineBySixteen (9:16)
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Kling3p0StandardMultiFunctionVideoGenAspectRatio>, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
// Direct mappings
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::SixteenByNine))
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::NineBySixteen))
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::Square))
|
||||
}
|
||||
|
||||
// Auto defaults to 16:9
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => {
|
||||
Ok(Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::SixteenByNine))
|
||||
}
|
||||
|
||||
// Mismatches
|
||||
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) -> Kling3p0StandardMultiFunctionVideoGenAspectRatio {
|
||||
match aspect_ratio {
|
||||
// Wide-ish → 16:9
|
||||
CommonAspectRatio::WideFourByThree
|
||||
| CommonAspectRatio::WideFiveByFour
|
||||
| CommonAspectRatio::WideThreeByTwo
|
||||
| CommonAspectRatio::WideTwentyOneByNine => Kling3p0StandardMultiFunctionVideoGenAspectRatio::SixteenByNine,
|
||||
// Tall-ish → 9:16
|
||||
CommonAspectRatio::TallThreeByFour
|
||||
| CommonAspectRatio::TallFourByFive
|
||||
| CommonAspectRatio::TallTwoByThree
|
||||
| CommonAspectRatio::TallNineByTwentyOne => Kling3p0StandardMultiFunctionVideoGenAspectRatio::NineBySixteen,
|
||||
// Fallback
|
||||
_ => Kling3p0StandardMultiFunctionVideoGenAspectRatio::Square,
|
||||
}
|
||||
}
|
||||
|
||||
// Kling 3.0 Standard supports durations of 3–15 seconds.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Kling3p0StandardMultiFunctionVideoGenDuration>, ArtcraftRouterError> {
|
||||
const MIN: u16 = 3;
|
||||
const MAX: u16 = 15;
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(Some(seconds_to_duration(d))),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(seconds_to_duration(d.clamp(MIN, MAX)))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn seconds_to_duration(seconds: u16) -> Kling3p0StandardMultiFunctionVideoGenDuration {
|
||||
match seconds {
|
||||
3 => Kling3p0StandardMultiFunctionVideoGenDuration::ThreeSeconds,
|
||||
4 => Kling3p0StandardMultiFunctionVideoGenDuration::FourSeconds,
|
||||
5 => Kling3p0StandardMultiFunctionVideoGenDuration::FiveSeconds,
|
||||
6 => Kling3p0StandardMultiFunctionVideoGenDuration::SixSeconds,
|
||||
7 => Kling3p0StandardMultiFunctionVideoGenDuration::SevenSeconds,
|
||||
8 => Kling3p0StandardMultiFunctionVideoGenDuration::EightSeconds,
|
||||
9 => Kling3p0StandardMultiFunctionVideoGenDuration::NineSeconds,
|
||||
10 => Kling3p0StandardMultiFunctionVideoGenDuration::TenSeconds,
|
||||
11 => Kling3p0StandardMultiFunctionVideoGenDuration::ElevenSeconds,
|
||||
12 => Kling3p0StandardMultiFunctionVideoGenDuration::TwelveSeconds,
|
||||
13 => Kling3p0StandardMultiFunctionVideoGenDuration::ThirteenSeconds,
|
||||
14 => Kling3p0StandardMultiFunctionVideoGenDuration::FourteenSeconds,
|
||||
_ => Kling3p0StandardMultiFunctionVideoGenDuration::FifteenSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
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::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;
|
||||
|
||||
fn base_request() -> GenerateVideoRequest<'static> {
|
||||
GenerateVideoRequest {
|
||||
model: CommonVideoModel::Kling3p0Standard,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_16x9() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftKling3p0Standard(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
assert!(matches!(plan.aspect_ratio, Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::SixteenByNine)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_9x16() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftKling3p0Standard(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
assert!(matches!(plan.aspect_ratio, Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::NineBySixteen)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_square() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftKling3p0Standard(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
assert!(matches!(plan.aspect_ratio, Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::Square)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_error_out_on_unsupported() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_valid_range() {
|
||||
for d in 3..=15 {
|
||||
let request = GenerateVideoRequest {
|
||||
duration_seconds: Some(d),
|
||||
..base_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_ok(), "duration {} should be valid", d);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_out_of_range_error_out() {
|
||||
let request = GenerateVideoRequest {
|
||||
duration_seconds: Some(16),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_image_ref_returns_error() {
|
||||
let request = GenerateVideoRequest {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/image.jpg")),
|
||||
..base_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
}
|
||||
-285
@@ -1,285 +0,0 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::{
|
||||
Seedance1p5ProMultiFunctionVideoGenAspectRatio,
|
||||
Seedance1p5ProMultiFunctionVideoGenDuration,
|
||||
Seedance1p5ProMultiFunctionVideoGenResolution,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftSeedance1p5Pro<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub start_frame: Option<&'a MediaFileToken>,
|
||||
pub end_frame: Option<&'a MediaFileToken>,
|
||||
pub aspect_ratio: Option<Seedance1p5ProMultiFunctionVideoGenAspectRatio>,
|
||||
pub duration: Option<Seedance1p5ProMultiFunctionVideoGenDuration>,
|
||||
pub resolution: Option<Seedance1p5ProMultiFunctionVideoGenResolution>,
|
||||
pub generate_audio: Option<bool>,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_seedance1p5_pro<'a>(
|
||||
request: &'a GenerateVideoRequest<'a>,
|
||||
) -> Result<PlanArtcraftSeedance1p5Pro<'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 aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftSeedance1p5Pro {
|
||||
prompt: request.prompt,
|
||||
start_frame,
|
||||
end_frame,
|
||||
aspect_ratio,
|
||||
duration,
|
||||
resolution,
|
||||
generate_audio: request.generate_audio,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Supported aspect ratios and their AR values (width / height):
|
||||
// NineBySixteen = 0.5625, ThreeByFour = 0.75, Square = 1.0, FourByThree = 1.33,
|
||||
// SixteenByNine = 1.78, TwentyOneByNine = 2.33, Auto = auto
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Seedance1p5ProMultiFunctionVideoGenAspectRatio>, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Some(Seedance1p5ProMultiFunctionVideoGenAspectRatio::Auto)),
|
||||
|
||||
// Direct mappings
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(Some(Seedance1p5ProMultiFunctionVideoGenAspectRatio::SixteenByNine))
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(Some(Seedance1p5ProMultiFunctionVideoGenAspectRatio::NineBySixteen))
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(Some(Seedance1p5ProMultiFunctionVideoGenAspectRatio::Square))
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => {
|
||||
Ok(Some(Seedance1p5ProMultiFunctionVideoGenAspectRatio::FourByThree))
|
||||
}
|
||||
Some(CommonAspectRatio::TallThreeByFour) => {
|
||||
Ok(Some(Seedance1p5ProMultiFunctionVideoGenAspectRatio::ThreeByFour))
|
||||
}
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => {
|
||||
Ok(Some(Seedance1p5ProMultiFunctionVideoGenAspectRatio::TwentyOneByNine))
|
||||
}
|
||||
|
||||
// Mismatches — apply strategy
|
||||
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) -> Seedance1p5ProMultiFunctionVideoGenAspectRatio {
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => Seedance1p5ProMultiFunctionVideoGenAspectRatio::FourByThree, // 1.25, nearest 1.33
|
||||
CommonAspectRatio::WideThreeByTwo => Seedance1p5ProMultiFunctionVideoGenAspectRatio::FourByThree, // 1.50, nearest 1.33
|
||||
CommonAspectRatio::TallFourByFive => Seedance1p5ProMultiFunctionVideoGenAspectRatio::ThreeByFour, // 0.80, nearest 0.75
|
||||
CommonAspectRatio::TallTwoByThree => Seedance1p5ProMultiFunctionVideoGenAspectRatio::ThreeByFour, // 0.67, nearest 0.75
|
||||
CommonAspectRatio::TallNineByTwentyOne => Seedance1p5ProMultiFunctionVideoGenAspectRatio::NineBySixteen, // 0.43, nearest 0.56
|
||||
_ => Seedance1p5ProMultiFunctionVideoGenAspectRatio::Square,
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 1.5 Pro supports durations of 4–12 seconds.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Seedance1p5ProMultiFunctionVideoGenDuration>, ArtcraftRouterError> {
|
||||
const MIN: u16 = 4;
|
||||
const MAX: u16 = 12;
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(Some(seconds_to_duration(d))),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(seconds_to_duration(d.clamp(MIN, MAX)))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn seconds_to_duration(seconds: u16) -> Seedance1p5ProMultiFunctionVideoGenDuration {
|
||||
match seconds {
|
||||
4 => Seedance1p5ProMultiFunctionVideoGenDuration::FourSeconds,
|
||||
5 => Seedance1p5ProMultiFunctionVideoGenDuration::FiveSeconds,
|
||||
6 => Seedance1p5ProMultiFunctionVideoGenDuration::SixSeconds,
|
||||
7 => Seedance1p5ProMultiFunctionVideoGenDuration::SevenSeconds,
|
||||
8 => Seedance1p5ProMultiFunctionVideoGenDuration::EightSeconds,
|
||||
9 => Seedance1p5ProMultiFunctionVideoGenDuration::NineSeconds,
|
||||
10 => Seedance1p5ProMultiFunctionVideoGenDuration::TenSeconds,
|
||||
11 => Seedance1p5ProMultiFunctionVideoGenDuration::ElevenSeconds,
|
||||
_ => Seedance1p5ProMultiFunctionVideoGenDuration::TwelveSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(
|
||||
resolution: Option<crate::api::common_resolution::CommonResolution>,
|
||||
_strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Seedance1p5ProMultiFunctionVideoGenResolution>, ArtcraftRouterError> {
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
Some(CommonResolution::OneK) => Ok(Some(Seedance1p5ProMultiFunctionVideoGenResolution::SevenTwentyP)),
|
||||
Some(CommonResolution::TwoK) => Ok(Some(Seedance1p5ProMultiFunctionVideoGenResolution::TenEightyP)),
|
||||
Some(CommonResolution::ThreeK) => Ok(Some(Seedance1p5ProMultiFunctionVideoGenResolution::TenEightyP)),
|
||||
Some(CommonResolution::FourK) => Ok(Some(Seedance1p5ProMultiFunctionVideoGenResolution::TenEightyP)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
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::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;
|
||||
|
||||
fn base_seedance_1p5_pro_request() -> GenerateVideoRequest<'static> {
|
||||
GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance1p5Pro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_16x9() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_seedance_1p5_pro_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance1p5Pro(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance1p5ProMultiFunctionVideoGenAspectRatio::SixteenByNine)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_9x16() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_seedance_1p5_pro_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance1p5Pro(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance1p5ProMultiFunctionVideoGenAspectRatio::NineBySixteen)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_21x9() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideTwentyOneByNine),
|
||||
..base_seedance_1p5_pro_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance1p5Pro(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance1p5ProMultiFunctionVideoGenAspectRatio::TwentyOneByNine)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_error_out_on_unsupported() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_seedance_1p5_pro_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_valid_range() {
|
||||
for d in 4..=12 {
|
||||
let request = GenerateVideoRequest {
|
||||
duration_seconds: Some(d),
|
||||
..base_seedance_1p5_pro_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_ok(), "duration {} should be valid", d);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_out_of_range_error_out() {
|
||||
let request = GenerateVideoRequest {
|
||||
duration_seconds: Some(13),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_seedance_1p5_pro_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_image_ref_returns_error() {
|
||||
let request = GenerateVideoRequest {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/image.jpg")),
|
||||
..base_seedance_1p5_pro_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -202,7 +202,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_video_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Landscape16x9)));
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_video_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Portrait9x16)));
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
..base_video_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Square1x1)));
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_video_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(
|
||||
matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Standard4x3)),
|
||||
"Expected Standard4x3 for WideThreeByTwo with {:?}", strategy,
|
||||
@@ -283,15 +283,15 @@ mod tests {
|
||||
#[test]
|
||||
fn batch_count_direct_mapping() {
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(1), ..base_video_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap() else { panic!("wrong variant") };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::One));
|
||||
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(2), ..base_video_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap() else { panic!("wrong variant") };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Two));
|
||||
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(4), ..base_video_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap() else { panic!("wrong variant") };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Four));
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_video_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Four));
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
..base_video_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap() else { panic!("wrong variant") };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Two));
|
||||
}
|
||||
|
||||
|
||||
-33
@@ -1,25 +1,13 @@
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_kling3p0_pro::estimate_video_cost_artcraft_kling3p0_pro;
|
||||
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_kling3p0_standard::estimate_video_cost_artcraft_kling3p0_standard;
|
||||
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance1p5_pro::estimate_video_cost_artcraft_seedance1p5_pro;
|
||||
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance2p0::estimate_video_cost_artcraft_seedance2p0;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling3p0_pro::execute_artcraft_kling3p0_pro;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling3p0_standard::execute_artcraft_kling3p0_standard;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_seedance1p5_pro::execute_artcraft_seedance1p5_pro;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_seedance2p0::execute_artcraft_seedance2p0;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_pro::PlanArtcraftKling3p0Pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::PlanArtcraftKling3p0Standard;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance1p5_pro::PlanArtcraftSeedance1p5Pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum VideoGenerationPlan<'a> {
|
||||
ArtcraftKling3p0Pro(PlanArtcraftKling3p0Pro<'a>),
|
||||
ArtcraftKling3p0Standard(PlanArtcraftKling3p0Standard<'a>),
|
||||
ArtcraftSeedance1p5Pro(PlanArtcraftSeedance1p5Pro<'a>),
|
||||
ArtcraftSeedance2p0(PlanArtcraftSeedance2p0<'a>),
|
||||
}
|
||||
|
||||
@@ -29,18 +17,6 @@ impl<'a> VideoGenerationPlan<'a> {
|
||||
client: &RouterClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
VideoGenerationPlan::ArtcraftKling3p0Pro(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_kling3p0_pro(plan, artcraft_client).await
|
||||
}
|
||||
VideoGenerationPlan::ArtcraftKling3p0Standard(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_kling3p0_standard(plan, artcraft_client).await
|
||||
}
|
||||
VideoGenerationPlan::ArtcraftSeedance1p5Pro(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_seedance1p5_pro(plan, artcraft_client).await
|
||||
}
|
||||
VideoGenerationPlan::ArtcraftSeedance2p0(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_seedance2p0(plan, artcraft_client).await
|
||||
@@ -50,15 +26,6 @@ impl<'a> VideoGenerationPlan<'a> {
|
||||
|
||||
pub fn estimate_costs(&self) -> VideoGenerationCostEstimate {
|
||||
match self {
|
||||
VideoGenerationPlan::ArtcraftKling3p0Pro(plan) => {
|
||||
estimate_video_cost_artcraft_kling3p0_pro(plan)
|
||||
}
|
||||
VideoGenerationPlan::ArtcraftKling3p0Standard(plan) => {
|
||||
estimate_video_cost_artcraft_kling3p0_standard(plan)
|
||||
}
|
||||
VideoGenerationPlan::ArtcraftSeedance1p5Pro(plan) => {
|
||||
estimate_video_cost_artcraft_seedance1p5_pro(plan)
|
||||
}
|
||||
VideoGenerationPlan::ArtcraftSeedance2p0(plan) => {
|
||||
estimate_video_cost_artcraft_seedance2p0(plan)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod generate_image;
|
||||
pub mod generate_splat;
|
||||
pub mod generate_video;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user