mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-23 10:09:29 -04:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7fc884d73 | ||
|
|
42bb07c110 | ||
|
|
5afa81b57d | ||
|
|
81cadf35b0 | ||
|
|
d4b3579915 | ||
|
|
7cd8c0299b | ||
|
|
87296752a5 | ||
|
|
1416c5cd17 | ||
|
|
de29444626 | ||
|
|
41988a4462 | ||
|
|
4c0c536fb0 | ||
|
|
b15a5d532a | ||
|
|
8ae983ee32 | ||
|
|
279a79e60a | ||
|
|
ebd2a276f9 | ||
|
|
0d2f1cfa2c | ||
|
|
3592111019 | ||
|
|
9d12603d1f | ||
|
|
8902f77a18 | ||
|
|
6d91b5616f | ||
|
|
3783c194e3 |
Generated
+4
@@ -1,5 +1,9 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<codeStyleSettings language="Markdown">
|
||||
<option name="RIGHT_MARGIN" value="100" />
|
||||
<option name="WRAP_ON_TYPING" value="1" />
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Rust">
|
||||
<option name="RIGHT_MARGIN" value="180" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n \n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 14
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "002cdb3d086f3b4181e2997befa54bd8516cb1d3d3bb6e389862695318d5c299"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 16
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "29916e73a1784ea0b70ef4f637520b05efccc31e01840f681e9b042f82eb8f7e"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE tasks\n SET task_status = ?,\n on_failure_type = ?,\n on_failure_message = ?\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9b99ccd0baf9a4b74d8903f63e3a3dcf4c09b75cad978f0532388cbd743b7516"
|
||||
}
|
||||
+31
-20
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n jobs.on_success_result_batch_token as maybe_result_batch_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n jobs.failure_reason,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n jobs.on_success_result_batch_token as maybe_result_batch_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -135,6 +135,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "failure_reason",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 2048
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "maybe_result_entity_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -144,7 +154,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 15,
|
||||
"name": "maybe_result_entity_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -154,7 +164,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 16,
|
||||
"name": "maybe_result_batch_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -164,7 +174,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 17,
|
||||
"name": "maybe_model_weights_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -174,7 +184,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 18,
|
||||
"name": "maybe_tts_model_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -184,7 +194,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 19,
|
||||
"name": "maybe_voice_conversion_model_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -194,7 +204,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"ordinal": 20,
|
||||
"name": "maybe_tts_public_bucket_path",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -204,7 +214,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"ordinal": 21,
|
||||
"name": "maybe_voice_conversion_public_bucket_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -214,7 +224,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"ordinal": 22,
|
||||
"name": "maybe_media_file_public_bucket_directory_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -224,7 +234,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"ordinal": 23,
|
||||
"name": "maybe_media_file_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -234,7 +244,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"ordinal": 24,
|
||||
"name": "maybe_media_file_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -244,7 +254,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"ordinal": 25,
|
||||
"name": "maybe_assigned_worker",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -254,7 +264,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"ordinal": 26,
|
||||
"name": "maybe_assigned_cluster",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -264,7 +274,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"ordinal": 27,
|
||||
"name": "is_keepalive_required",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -274,7 +284,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"ordinal": 28,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -284,7 +294,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"ordinal": 29,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -294,7 +304,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"ordinal": 30,
|
||||
"name": "maybe_first_started_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -304,7 +314,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"ordinal": 31,
|
||||
"name": "maybe_successfully_completed_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -314,7 +324,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 31,
|
||||
"ordinal": 32,
|
||||
"name": "database_clock",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
@@ -354,6 +364,7 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -362,5 +373,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "243afe0bc574f9f233c7797add9faf96440e2bafd52032e07951272fb1ec78b1"
|
||||
"hash": "a2c9e2f61eba666254f8432b3c50a0a7738ee3b3faa77c5281bc8e4af96159b9"
|
||||
}
|
||||
Generated
+29
@@ -788,6 +788,8 @@ dependencies = [
|
||||
"artcraft_client",
|
||||
"fal_client",
|
||||
"log",
|
||||
"muapi_client",
|
||||
"reqwest 0.12.23",
|
||||
"seedance2pro",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -795,9 +797,11 @@ dependencies = [
|
||||
"test_data",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"url_utils",
|
||||
"uuid 1.18.1",
|
||||
"workspace-build-acceleration",
|
||||
"world_labs_api",
|
||||
"wreq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3934,6 +3938,7 @@ dependencies = [
|
||||
name = "enums"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -7228,6 +7233,29 @@ dependencies = [
|
||||
"thiserror 1.0.63",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muapi_client"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"cloudflare_errors",
|
||||
"env",
|
||||
"env_logger 0.11.9",
|
||||
"errors",
|
||||
"log",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"test_data",
|
||||
"testing",
|
||||
"tokio",
|
||||
"url",
|
||||
"workspace-build-acceleration",
|
||||
"wreq",
|
||||
"wreq-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.17.1"
|
||||
@@ -11597,6 +11625,7 @@ dependencies = [
|
||||
"tts_common",
|
||||
"url",
|
||||
"url_config",
|
||||
"url_utils",
|
||||
"user_input_common",
|
||||
"user_traits_component",
|
||||
"users",
|
||||
|
||||
@@ -16,6 +16,7 @@ members = [
|
||||
"crates/api_clients/midjourney_client",
|
||||
"crates/api_clients/openai_sora_client",
|
||||
"crates/api_clients/openart_client",
|
||||
"crates/api_clients/muapi_client",
|
||||
"crates/api_clients/seedance2pro",
|
||||
"crates/api_clients/runwayml_client",
|
||||
"crates/api_clients/thumbnail_generator",
|
||||
@@ -240,6 +241,7 @@ memory_store = { path = "crates/lib/caching/memory_store" }
|
||||
midjourney_client = { path = "crates/api_clients/midjourney_client" }
|
||||
mimetypes = { path = "crates/lib/files/mimetypes" }
|
||||
openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
|
||||
muapi_client = { path = "crates/api_clients/muapi_client" }
|
||||
seedance2pro = { path = "crates/api_clients/seedance2pro" }
|
||||
primitives = { path = "crates/lib/data/primitives" }
|
||||
shared_service_components = { path = "crates/lib/shared_service_components" }
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files
|
||||
DROP COLUMN
|
||||
maybe_thumbnail_version;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files
|
||||
ADD COLUMN
|
||||
maybe_thumbnail_version TINYINT UNSIGNED DEFAULT NULL;
|
||||
@@ -224,6 +224,12 @@ CREATE TABLE media_files (
|
||||
-- If so, we may not need to add a watermark downstream in further processing steps.
|
||||
has_watermark BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- Tracks the thumbnail algorithm/type used for this media file.
|
||||
-- NULL = no thumbnail generated yet.
|
||||
-- 1 = video thumbnail.
|
||||
-- Other values are reserved for future thumbnail algorithms or media-specific types.
|
||||
maybe_thumbnail_version TINYINT UNSIGNED DEFAULT NULL,
|
||||
|
||||
-- ========== UPLOAD, TRANSCODING, AND TRUNCATION DETAILS ==========
|
||||
|
||||
-- The hash for the bucket directory that contains the original upload
|
||||
|
||||
+6
@@ -23,6 +23,12 @@ pub struct Seedance2p0MultiFunctionVideoGenRequest {
|
||||
/// Optional reference images (reference mode). When present, takes priority over start/end frames.
|
||||
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Optional reference videos.
|
||||
pub reference_video_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Optional reference audio files.
|
||||
pub reference_audio_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Video aspect ratio / resolution.
|
||||
pub aspect_ratio: Option<Seedance2p0AspectRatio>,
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_api_clients::FrontendFailureCategoryForApiClients;
|
||||
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_old_clients::FrontendFailureCategoryForOldClients;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
|
||||
pub const LIST_SESSION_JOBS_URL_PATH: &str = "/v1/jobs/session";
|
||||
@@ -98,9 +100,19 @@ pub struct ListSessionStatusDetailsResponse {
|
||||
/// This is typically only for non-premium users.
|
||||
pub requires_keepalive: bool,
|
||||
|
||||
/// NB: DO NOT CHANGE: Old clients can't read the new enum payload values as they are introduced.
|
||||
///
|
||||
/// An enum the frontend can use to display localized/I18N error
|
||||
/// messages. These pertain to both transient and permanent failures.
|
||||
pub maybe_failure_category: Option<FrontendFailureCategory>,
|
||||
#[deprecated(note="use maybe_failure_category_updated")]
|
||||
pub maybe_failure_category: Option<FrontendFailureCategoryForOldClients>,
|
||||
|
||||
/// An enum the frontend can use to display localized/I18N error
|
||||
/// messages. These pertain to both transient and permanent failures.
|
||||
pub maybe_failure_category_updated: Option<FrontendFailureCategoryForApiClients>,
|
||||
|
||||
/// User-facing failure messages
|
||||
pub maybe_failure_message: Option<String>,
|
||||
|
||||
/// This is an integer number between 0 and 100 (both inclusive) that
|
||||
/// reports the completeness.
|
||||
@@ -125,4 +137,3 @@ pub struct ListSessionResultDetailsResponse {
|
||||
|
||||
pub maybe_successfully_completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -19,14 +19,18 @@ workspace-build-acceleration.workspace = true
|
||||
artcraft_api_defs.workspace = true
|
||||
artcraft_client.workspace = true
|
||||
fal_client.workspace = true
|
||||
muapi_client.workspace = true
|
||||
seedance2pro.workspace = true
|
||||
tokens.workspace = true
|
||||
url_utils.workspace = true
|
||||
world_labs_api.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
wreq.workspace = true
|
||||
serde_derive.workspace = true
|
||||
tokio.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum AudioListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod audio_list_ref;
|
||||
pub mod common_aspect_ratio;
|
||||
pub mod common_image_model;
|
||||
pub mod common_resolution;
|
||||
@@ -6,3 +7,4 @@ pub mod common_video_model;
|
||||
pub mod image_list_ref;
|
||||
pub mod image_ref;
|
||||
pub mod provider;
|
||||
pub mod video_list_ref;
|
||||
|
||||
@@ -6,4 +6,6 @@ use serde_derive::{Deserialize, Serialize};
|
||||
pub enum Provider {
|
||||
Artcraft,
|
||||
Fal,
|
||||
Muapi,
|
||||
Seedance2Pro,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum VideoListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
}
|
||||
@@ -5,3 +5,5 @@ pub mod request_mismatch_mitigation_strategy;
|
||||
pub mod router_artcraft_client;
|
||||
pub mod router_client;
|
||||
pub mod router_fal_client;
|
||||
pub mod router_muapi_client;
|
||||
pub mod router_seedance2pro_client;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_muapi_client::RouterMuapiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::client_error::{ClientError, ClientType};
|
||||
|
||||
pub struct MultiRouterClient {
|
||||
pub(crate) artcraft_client: Option<RouterArtcraftClient>,
|
||||
pub(crate) fal_client: Option<RouterFalClient>,
|
||||
pub(crate) muapi_client: Option<RouterMuapiClient>,
|
||||
pub(crate) seedance2pro_client: Option<RouterSeedance2ProClient>,
|
||||
}
|
||||
|
||||
impl MultiRouterClient {
|
||||
@@ -17,4 +21,14 @@ impl MultiRouterClient {
|
||||
self.fal_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::Fal))
|
||||
}
|
||||
|
||||
pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> {
|
||||
self.muapi_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::Muapi))
|
||||
}
|
||||
|
||||
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ClientError> {
|
||||
self.seedance2pro_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::Seedance2Pro))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use crate::client::multi_router_client::MultiRouterClient;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_muapi_client::RouterMuapiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
|
||||
pub struct MultiRouterClientBuilder {
|
||||
artcraft_client: Option<RouterArtcraftClient>,
|
||||
fal_client: Option<RouterFalClient>,
|
||||
muapi_client: Option<RouterMuapiClient>,
|
||||
seedance2pro_client: Option<RouterSeedance2ProClient>,
|
||||
}
|
||||
|
||||
impl MultiRouterClientBuilder {
|
||||
@@ -12,6 +16,8 @@ impl MultiRouterClientBuilder {
|
||||
Self {
|
||||
artcraft_client: None,
|
||||
fal_client: None,
|
||||
muapi_client: None,
|
||||
seedance2pro_client: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +31,22 @@ impl MultiRouterClientBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_muapi_client(mut self, client: RouterMuapiClient) -> Self {
|
||||
self.muapi_client = Some(client);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_seedance2pro_client(mut self, client: RouterSeedance2ProClient) -> Self {
|
||||
self.seedance2pro_client = Some(client);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> MultiRouterClient {
|
||||
MultiRouterClient {
|
||||
artcraft_client: self.artcraft_client,
|
||||
fal_client: self.fal_client,
|
||||
muapi_client: self.muapi_client,
|
||||
seedance2pro_client: self.seedance2pro_client,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use crate::client::multi_router_client::MultiRouterClient;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_muapi_client::RouterMuapiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::client_error::{ClientError, ClientType};
|
||||
|
||||
pub enum RouterClient {
|
||||
Multi(MultiRouterClient),
|
||||
Artcraft(RouterArtcraftClient),
|
||||
Fal(RouterFalClient),
|
||||
Muapi(RouterMuapiClient),
|
||||
Seedance2Pro(RouterSeedance2ProClient),
|
||||
}
|
||||
|
||||
impl RouterClient {
|
||||
@@ -25,4 +29,20 @@ impl RouterClient {
|
||||
_ => Err(ClientError::ClientNotConfigured(ClientType::Fal)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> {
|
||||
match self {
|
||||
RouterClient::Muapi(client) => Ok(client),
|
||||
RouterClient::Multi(multi) => multi.get_muapi_client_ref(),
|
||||
_ => Err(ClientError::ClientNotConfigured(ClientType::Muapi)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ClientError> {
|
||||
match self {
|
||||
RouterClient::Seedance2Pro(client) => Ok(client),
|
||||
RouterClient::Multi(multi) => multi.get_seedance2pro_client_ref(),
|
||||
_ => Err(ClientError::ClientNotConfigured(ClientType::Seedance2Pro)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use muapi_client::creds::muapi_session::MuapiSession;
|
||||
|
||||
pub struct RouterMuapiClient {
|
||||
pub(crate) session: MuapiSession,
|
||||
}
|
||||
|
||||
impl RouterMuapiClient {
|
||||
pub fn new(session: MuapiSession) -> Self {
|
||||
RouterMuapiClient { session }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use seedance2pro::creds::seedance2pro_session::Seedance2ProSession;
|
||||
|
||||
pub struct RouterSeedance2ProClient {
|
||||
pub(crate) session: Seedance2ProSession,
|
||||
}
|
||||
|
||||
impl RouterSeedance2ProClient {
|
||||
pub fn new(session: Seedance2ProSession) -> Self {
|
||||
RouterSeedance2ProClient { session }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::errors::download_error::DownloadError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
@@ -8,6 +9,9 @@ pub enum ArtcraftRouterError {
|
||||
/// A client configuration error.
|
||||
Client(ClientError),
|
||||
|
||||
/// Failed to download a file from a URL (e.g. when re-uploading to a provider's CDN).
|
||||
Download(DownloadError),
|
||||
|
||||
/// The requested model is not yet supported by the router.
|
||||
UnsupportedModel(String),
|
||||
|
||||
@@ -24,6 +28,7 @@ impl Display for ArtcraftRouterError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Client(e) => write!(f, "Client error: {}", e),
|
||||
Self::Download(e) => write!(f, "Download error: {}", e),
|
||||
Self::UnsupportedModel(model) => write!(f, "Unsupported model: {}", model),
|
||||
Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
|
||||
Self::Provider(e) => write!(f, "Provider error: {}", e),
|
||||
@@ -37,6 +42,12 @@ impl From<ClientError> for ArtcraftRouterError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DownloadError> for ArtcraftRouterError {
|
||||
fn from(error: DownloadError) -> Self {
|
||||
Self::Download(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProviderError> for ArtcraftRouterError {
|
||||
fn from(error: ProviderError) -> Self {
|
||||
Self::Provider(error)
|
||||
|
||||
@@ -5,6 +5,8 @@ use std::fmt::{Display, Formatter};
|
||||
pub enum ClientType {
|
||||
Artcraft,
|
||||
Fal,
|
||||
Muapi,
|
||||
Seedance2Pro,
|
||||
}
|
||||
|
||||
impl Display for ClientType {
|
||||
@@ -12,6 +14,8 @@ impl Display for ClientType {
|
||||
match self {
|
||||
Self::Artcraft => write!(f, "Artcraft"),
|
||||
Self::Fal => write!(f, "Fal"),
|
||||
Self::Muapi => write!(f, "Muapi"),
|
||||
Self::Seedance2Pro => write!(f, "Seedance2Pro"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +37,12 @@ pub enum ClientError {
|
||||
|
||||
/// Fal only accepts image URLs for image inputs, not media tokens.
|
||||
FalOnlySupportsUrls,
|
||||
|
||||
/// Muapi only accepts URLs for image inputs, not media tokens.
|
||||
MuapiOnlySupportsUrls,
|
||||
|
||||
/// Seedance2Pro only accepts URLs for media inputs, not media tokens.
|
||||
Seedance2ProOnlySupportsUrls,
|
||||
}
|
||||
|
||||
impl Error for ClientError {}
|
||||
@@ -55,6 +65,12 @@ impl Display for ClientError {
|
||||
Self::FalOnlySupportsUrls => {
|
||||
write!(f, "Fal only supports image URLs for image inputs, not media tokens")
|
||||
}
|
||||
Self::MuapiOnlySupportsUrls => {
|
||||
write!(f, "Muapi only supports URLs for image inputs; resolve media tokens to URLs before calling this provider")
|
||||
}
|
||||
Self::Seedance2ProOnlySupportsUrls => {
|
||||
write!(f, "Seedance2Pro only supports URLs for media inputs; resolve media tokens to URLs before calling this provider")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DownloadError {
|
||||
BadStatus {
|
||||
url: String,
|
||||
status_code: u16,
|
||||
},
|
||||
|
||||
ReqwestDownload {
|
||||
url: String,
|
||||
error: reqwest::Error,
|
||||
maybe_status: Option<reqwest::StatusCode>,
|
||||
},
|
||||
|
||||
WreqDownload {
|
||||
url: String,
|
||||
error: wreq::Error,
|
||||
maybe_status: Option<wreq::StatusCode>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Error for DownloadError {}
|
||||
|
||||
impl Display for DownloadError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::BadStatus { url, status_code } => {
|
||||
write!(f, "Download failed for {} with status {}", url, status_code)
|
||||
}
|
||||
Self::ReqwestDownload { url, error, maybe_status } => {
|
||||
write!(f, "Reqwest download error for {}: {} (status: {:?})", url, error, maybe_status)
|
||||
}
|
||||
Self::WreqDownload { url, error, maybe_status } => {
|
||||
write!(f, "Wreq download error for {}: {} (status: {:?})", url, error, maybe_status)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod artcraft_router_error;
|
||||
pub mod client_error;
|
||||
pub mod download_error;
|
||||
pub mod provider_error;
|
||||
|
||||
@@ -2,11 +2,15 @@ use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use artcraft_client::error::storyteller_error::StorytellerError;
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use muapi_client::error::muapi_error::MuapiError;
|
||||
use seedance2pro::error::seedance2pro_error::Seedance2ProError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProviderError {
|
||||
Storyteller(StorytellerError),
|
||||
Fal(FalErrorPlus),
|
||||
Muapi(MuapiError),
|
||||
Seedance2Pro(Seedance2ProError),
|
||||
}
|
||||
|
||||
impl Error for ProviderError {}
|
||||
@@ -16,6 +20,8 @@ impl Display for ProviderError {
|
||||
match self {
|
||||
Self::Storyteller(e) => write!(f, "Storyteller provider error: {}", e),
|
||||
Self::Fal(e) => write!(f, "Fal provider error: {}", e),
|
||||
Self::Muapi(e) => write!(f, "Muapi provider error: {}", e),
|
||||
Self::Seedance2Pro(e) => write!(f, "Seedance2Pro provider error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,3 +37,15 @@ impl From<FalErrorPlus> for ProviderError {
|
||||
Self::Fal(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MuapiError> for ProviderError {
|
||||
fn from(error: MuapiError) -> Self {
|
||||
Self::Muapi(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Seedance2ProError> for ProviderError {
|
||||
fn from(error: Seedance2ProError) -> Self {
|
||||
Self::Seedance2Pro(error)
|
||||
}
|
||||
}
|
||||
|
||||
+39
-63
@@ -73,72 +73,48 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
/// Read the image generation request, construct a plan, then yield a means to execute it.
|
||||
pub fn build(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.provider {
|
||||
Provider::Artcraft => match self.model {
|
||||
CommonImageModel::Flux1Dev => {
|
||||
plan_generate_image_artcraft_flux_1_dev(self).map(ImageGenerationPlan::ArtcraftFlux1Dev)
|
||||
}
|
||||
CommonImageModel::Flux1Schnell => {
|
||||
plan_generate_image_artcraft_flux_1_schnell(self).map(ImageGenerationPlan::ArtcraftFlux1Schnell)
|
||||
}
|
||||
CommonImageModel::FluxPro11 => {
|
||||
plan_generate_image_artcraft_flux_pro_1p1(self).map(ImageGenerationPlan::ArtcraftFluxPro11)
|
||||
}
|
||||
CommonImageModel::FluxPro11Ultra => {
|
||||
plan_generate_image_artcraft_flux_pro_1p1_ultra(self).map(ImageGenerationPlan::ArtcraftFluxPro11Ultra)
|
||||
}
|
||||
CommonImageModel::GptImage1p5 => {
|
||||
plan_generate_image_artcraft_gpt_image_1p5(self).map(ImageGenerationPlan::ArtcraftGptImage1p5)
|
||||
}
|
||||
CommonImageModel::NanaBanana => {
|
||||
plan_generate_image_artcraft_nano_banana(self).map(ImageGenerationPlan::ArtcraftNanaBanana)
|
||||
}
|
||||
CommonImageModel::NanaBanana2 => {
|
||||
plan_generate_image_artcraft_nano_banana_2(self).map(ImageGenerationPlan::ArtcraftNanaBanana2)
|
||||
}
|
||||
CommonImageModel::NanaBananaPro => {
|
||||
plan_generate_image_artcraft_nano_banana_pro(self).map(ImageGenerationPlan::ArtcraftNanaBananaPro)
|
||||
}
|
||||
CommonImageModel::Seedream4 => {
|
||||
plan_generate_image_artcraft_seedream_4(self).map(ImageGenerationPlan::ArtcraftSeedream4)
|
||||
}
|
||||
CommonImageModel::Seedream4p5 => {
|
||||
plan_generate_image_artcraft_seedream_4p5(self).map(ImageGenerationPlan::ArtcraftSeedream4p5)
|
||||
}
|
||||
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 => {
|
||||
plan_generate_image_fal_nano_banana_pro(self).map(ImageGenerationPlan::FalNanaBananaPro)
|
||||
}
|
||||
CommonImageModel::Flux1Dev
|
||||
| CommonImageModel::Flux1Schnell
|
||||
| CommonImageModel::FluxPro11
|
||||
| CommonImageModel::FluxPro11Ultra
|
||||
| CommonImageModel::GptImage1p5
|
||||
| CommonImageModel::NanaBanana
|
||||
| CommonImageModel::NanaBanana2
|
||||
| CommonImageModel::Seedream4
|
||||
| CommonImageModel::Seedream4p5
|
||||
| CommonImageModel::Seedream5Lite
|
||||
| CommonImageModel::QwenEdit2511Angles
|
||||
| CommonImageModel::Flux2LoraAngles => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "provider",
|
||||
value: format!("{:?} is only available on the Artcraft provider", self.model),
|
||||
}))
|
||||
}
|
||||
},
|
||||
Provider::Artcraft => self.build_artcraft(),
|
||||
Provider::Fal => self.build_fal(),
|
||||
_ => self.unsupported_provider(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_artcraft(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.model {
|
||||
CommonImageModel::Flux1Dev => plan_generate_image_artcraft_flux_1_dev(self),
|
||||
CommonImageModel::Flux1Schnell => plan_generate_image_artcraft_flux_1_schnell(self),
|
||||
CommonImageModel::FluxPro11 => plan_generate_image_artcraft_flux_pro_1p1(self),
|
||||
CommonImageModel::FluxPro11Ultra => plan_generate_image_artcraft_flux_pro_1p1_ultra(self),
|
||||
CommonImageModel::GptImage1p5 => plan_generate_image_artcraft_gpt_image_1p5(self),
|
||||
CommonImageModel::NanaBanana => plan_generate_image_artcraft_nano_banana(self),
|
||||
CommonImageModel::NanaBanana2 => plan_generate_image_artcraft_nano_banana_2(self),
|
||||
CommonImageModel::NanaBananaPro => plan_generate_image_artcraft_nano_banana_pro(self),
|
||||
CommonImageModel::Seedream4 => plan_generate_image_artcraft_seedream_4(self),
|
||||
CommonImageModel::Seedream4p5 => plan_generate_image_artcraft_seedream_4p5(self),
|
||||
CommonImageModel::Seedream5Lite => plan_generate_image_artcraft_seedream_5_lite(self),
|
||||
CommonImageModel::QwenEdit2511Angles => plan_generate_image_artcraft_qwen_edit_2511_angles(self),
|
||||
CommonImageModel::Flux2LoraAngles => plan_generate_image_artcraft_flux_2_lora_angles(self),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_fal(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.model {
|
||||
CommonImageModel::NanaBananaPro => plan_generate_image_fal_nano_banana_pro(self),
|
||||
_ => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "provider",
|
||||
value: format!("{:?} is only available on the Artcraft provider", self.model),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_provider(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
Err(ArtcraftRouterError::UnsupportedModel(
|
||||
format!("Image generation for model `{:?}` is not supported for provider {:?}", self.model, self.provider)
|
||||
))
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
+4
-4
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::{
|
||||
GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageNumImages,
|
||||
};
|
||||
@@ -18,7 +19,7 @@ pub struct PlanArtcraftFlux1Dev<'a> {
|
||||
|
||||
pub fn plan_generate_image_artcraft_flux_1_dev<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftFlux1Dev<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
// Flux 1 Dev is text-to-image only. Abort if image inputs are provided and
|
||||
// the caller has opted into strict mode.
|
||||
if request.image_inputs.is_some() {
|
||||
@@ -35,12 +36,12 @@ pub fn plan_generate_image_artcraft_flux_1_dev<'a>(
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftFlux1Dev {
|
||||
Ok(ImageGenerationPlan::ArtcraftFlux1Dev(PlanArtcraftFlux1Dev {
|
||||
prompt: request.prompt,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
// Flux 1 Dev supported aspect ratios:
|
||||
@@ -134,7 +135,6 @@ mod tests {
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_flux_1_dev_image_request;
|
||||
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::{
|
||||
GenerateFlux1DevTextToImageAspectRatio as FlAr,
|
||||
|
||||
+4
-4
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::{
|
||||
GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageNumImages,
|
||||
};
|
||||
@@ -18,7 +19,7 @@ pub struct PlanArtcraftFlux1Schnell<'a> {
|
||||
|
||||
pub fn plan_generate_image_artcraft_flux_1_schnell<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftFlux1Schnell<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
// Flux 1 Schnell is text-to-image only. Abort if image inputs are provided and
|
||||
// the caller has opted into strict mode.
|
||||
if request.image_inputs.is_some() {
|
||||
@@ -35,12 +36,12 @@ pub fn plan_generate_image_artcraft_flux_1_schnell<'a>(
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftFlux1Schnell {
|
||||
Ok(ImageGenerationPlan::ArtcraftFlux1Schnell(PlanArtcraftFlux1Schnell {
|
||||
prompt: request.prompt,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
// Flux 1 Schnell supported aspect ratios (identical to Flux 1 Dev):
|
||||
@@ -134,7 +135,6 @@ mod tests {
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_flux_1_schnell_image_request;
|
||||
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::{
|
||||
GenerateFlux1SchnellTextToImageAspectRatio as FlAr,
|
||||
|
||||
+4
-4
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
|
||||
Flux2LoraEditImageAngleImageSize,
|
||||
Flux2LoraEditImageAngleNumImages,
|
||||
@@ -23,7 +24,7 @@ pub struct PlanArtcraftFlux2LoraAngles<'a> {
|
||||
|
||||
pub fn plan_generate_image_artcraft_flux_2_lora_angles<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftFlux2LoraAngles<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Angle models require exactly one input image.
|
||||
@@ -31,7 +32,7 @@ pub fn plan_generate_image_artcraft_flux_2_lora_angles<'a>(
|
||||
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftFlux2LoraAngles {
|
||||
Ok(ImageGenerationPlan::ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles {
|
||||
image_input,
|
||||
horizontal_angle: request.horizontal_angle,
|
||||
vertical_angle: request.vertical_angle,
|
||||
@@ -39,7 +40,7 @@ pub fn plan_generate_image_artcraft_flux_2_lora_angles<'a>(
|
||||
image_size,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_single_image_input<'a>(
|
||||
@@ -139,7 +140,6 @@ fn plan_num_images(
|
||||
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,
|
||||
|
||||
+4
-4
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::{
|
||||
GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageNumImages,
|
||||
};
|
||||
@@ -18,7 +19,7 @@ pub struct PlanArtcraftFluxPro11<'a> {
|
||||
|
||||
pub fn plan_generate_image_artcraft_flux_pro_1p1<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftFluxPro11<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
// Flux Pro 1.1 is text-to-image only. Abort if image inputs are provided and
|
||||
// the caller has opted into strict mode.
|
||||
if request.image_inputs.is_some() {
|
||||
@@ -35,12 +36,12 @@ pub fn plan_generate_image_artcraft_flux_pro_1p1<'a>(
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftFluxPro11 {
|
||||
Ok(ImageGenerationPlan::ArtcraftFluxPro11(PlanArtcraftFluxPro11 {
|
||||
prompt: request.prompt,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
// Flux Pro 1.1 supported aspect ratios (no SquareHd):
|
||||
@@ -134,7 +135,6 @@ mod tests {
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_flux_pro_1p1_image_request;
|
||||
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::{
|
||||
GenerateFluxPro11TextToImageAspectRatio as FlAr,
|
||||
|
||||
+4
-4
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::{
|
||||
GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageNumImages,
|
||||
};
|
||||
@@ -18,7 +19,7 @@ pub struct PlanArtcraftFluxPro11Ultra<'a> {
|
||||
|
||||
pub fn plan_generate_image_artcraft_flux_pro_1p1_ultra<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftFluxPro11Ultra<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
// Flux Pro 1.1 Ultra is text-to-image only. Abort if image inputs are provided and
|
||||
// the caller has opted into strict mode.
|
||||
if request.image_inputs.is_some() {
|
||||
@@ -35,12 +36,12 @@ pub fn plan_generate_image_artcraft_flux_pro_1p1_ultra<'a>(
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftFluxPro11Ultra {
|
||||
Ok(ImageGenerationPlan::ArtcraftFluxPro11Ultra(PlanArtcraftFluxPro11Ultra {
|
||||
prompt: request.prompt,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
// Flux Pro 1.1 Ultra supported aspect ratios:
|
||||
@@ -137,7 +138,6 @@ mod tests {
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_flux_pro_1p1_ultra_image_request;
|
||||
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::{
|
||||
GenerateFluxPro11UltraTextToImageAspectRatio as FlAr,
|
||||
|
||||
+4
-4
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{
|
||||
GptImage1p5MultiFunctionImageGenNumImages, GptImage1p5MultiFunctionImageGenQuality,
|
||||
GptImage1p5MultiFunctionImageGenSize,
|
||||
@@ -24,21 +25,21 @@ pub struct PlanArtcraftGptImage1p5<'a> {
|
||||
|
||||
pub fn plan_generate_image_artcraft_gpt_image_1p5<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftGptImage1p5<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let image_inputs = resolve_image_list_ref(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(PlanArtcraftGptImage1p5 {
|
||||
Ok(ImageGenerationPlan::ArtcraftGptImage1p5(PlanArtcraftGptImage1p5 {
|
||||
prompt: request.prompt,
|
||||
image_inputs,
|
||||
image_size,
|
||||
quality: GptImage1p5MultiFunctionImageGenQuality::Medium,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
@@ -123,7 +124,6 @@ mod tests {
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_gpt_image_1p5_image_request;
|
||||
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{
|
||||
GptImage1p5MultiFunctionImageGenNumImages as GN,
|
||||
|
||||
+4
-4
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::{
|
||||
NanoBananaMultiFunctionImageGenAspectRatio, NanoBananaMultiFunctionImageGenNumImages,
|
||||
};
|
||||
@@ -21,7 +22,7 @@ pub struct PlanArtcraftNanaBanana<'a> {
|
||||
|
||||
pub fn plan_generate_image_artcraft_nano_banana<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftNanaBanana<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
@@ -29,13 +30,13 @@ pub fn plan_generate_image_artcraft_nano_banana<'a>(
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftNanaBanana {
|
||||
Ok(ImageGenerationPlan::ArtcraftNanaBanana(PlanArtcraftNanaBanana {
|
||||
prompt: request.prompt,
|
||||
image_inputs,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
@@ -148,7 +149,6 @@ mod tests {
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_nano_banana_image_request;
|
||||
use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::{
|
||||
NanoBananaMultiFunctionImageGenAspectRatio as NbAr,
|
||||
|
||||
+4
-4
@@ -5,6 +5,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::{
|
||||
NanaBanana2MultiFunctionImageGenAspectRatio, NanaBanana2MultiFunctionImageGenImageResolution,
|
||||
NanaBanana2MultiFunctionImageGenNumImages,
|
||||
@@ -24,7 +25,7 @@ pub struct PlanArtcraftNanaBanana2<'a> {
|
||||
|
||||
pub fn plan_generate_image_artcraft_nano_banana_2<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftNanaBanana2<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
@@ -33,14 +34,14 @@ pub fn plan_generate_image_artcraft_nano_banana_2<'a>(
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftNanaBanana2 {
|
||||
Ok(ImageGenerationPlan::ArtcraftNanaBanana2(PlanArtcraftNanaBanana2 {
|
||||
prompt: request.prompt,
|
||||
image_inputs,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
@@ -172,7 +173,6 @@ mod tests {
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_nano_banana_2_image_request;
|
||||
use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::{
|
||||
NanaBanana2MultiFunctionImageGenAspectRatio as Nb2Ar,
|
||||
|
||||
+4
-4
@@ -5,6 +5,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{
|
||||
NanoBananaProMultiFunctionImageGenAspectRatio, NanoBananaProMultiFunctionImageGenImageResolution,
|
||||
NanoBananaProMultiFunctionImageGenNumImages,
|
||||
@@ -24,7 +25,7 @@ pub struct PlanArtcraftNanaBananaPro<'a> {
|
||||
|
||||
pub fn plan_generate_image_artcraft_nano_banana_pro<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftNanaBananaPro<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
@@ -33,14 +34,14 @@ pub fn plan_generate_image_artcraft_nano_banana_pro<'a>(
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftNanaBananaPro {
|
||||
Ok(ImageGenerationPlan::ArtcraftNanaBananaPro(PlanArtcraftNanaBananaPro {
|
||||
prompt: request.prompt,
|
||||
image_inputs,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
@@ -172,7 +173,6 @@ mod tests {
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_image_request;
|
||||
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{
|
||||
NanoBananaProMultiFunctionImageGenAspectRatio as NbpAr,
|
||||
|
||||
+4
-4
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::{
|
||||
QwenEdit2511EditImageAngleImageSize,
|
||||
QwenEdit2511EditImageAngleNumImages,
|
||||
@@ -24,7 +25,7 @@ pub struct PlanArtcraftQwenEdit2511Angles<'a> {
|
||||
|
||||
pub fn plan_generate_image_artcraft_qwen_edit_2511_angles<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftQwenEdit2511Angles<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Angle models require exactly one input image.
|
||||
@@ -32,7 +33,7 @@ pub fn plan_generate_image_artcraft_qwen_edit_2511_angles<'a>(
|
||||
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftQwenEdit2511Angles {
|
||||
Ok(ImageGenerationPlan::ArtcraftQwenEdit2511Angles(PlanArtcraftQwenEdit2511Angles {
|
||||
prompt: request.prompt,
|
||||
image_input,
|
||||
horizontal_angle: request.horizontal_angle,
|
||||
@@ -41,7 +42,7 @@ pub fn plan_generate_image_artcraft_qwen_edit_2511_angles<'a>(
|
||||
image_size,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_single_image_input<'a>(
|
||||
@@ -141,7 +142,6 @@ fn plan_num_images(
|
||||
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,
|
||||
|
||||
+4
-4
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::{
|
||||
BytedanceSeedreamV4MultiFunctionImageGenImageSize,
|
||||
BytedanceSeedreamV4MultiFunctionImageGenNumImages,
|
||||
@@ -22,7 +23,7 @@ pub struct PlanArtcraftSeedream4<'a> {
|
||||
|
||||
pub fn plan_generate_image_artcraft_seedream_4<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftSeedream4<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
@@ -30,13 +31,13 @@ pub fn plan_generate_image_artcraft_seedream_4<'a>(
|
||||
let image_size = plan_image_size(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftSeedream4 {
|
||||
Ok(ImageGenerationPlan::ArtcraftSeedream4(PlanArtcraftSeedream4 {
|
||||
prompt: request.prompt,
|
||||
image_inputs,
|
||||
image_size,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
@@ -147,7 +148,6 @@ mod tests {
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_seedream_4_image_request;
|
||||
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::{
|
||||
BytedanceSeedreamV4MultiFunctionImageGenImageSize as S,
|
||||
|
||||
+4
-4
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::{
|
||||
BytedanceSeedreamV4p5MultiFunctionImageGenImageSize,
|
||||
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages,
|
||||
@@ -22,7 +23,7 @@ pub struct PlanArtcraftSeedream4p5<'a> {
|
||||
|
||||
pub fn plan_generate_image_artcraft_seedream_4p5<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftSeedream4p5<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
@@ -30,13 +31,13 @@ pub fn plan_generate_image_artcraft_seedream_4p5<'a>(
|
||||
let image_size = plan_image_size(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftSeedream4p5 {
|
||||
Ok(ImageGenerationPlan::ArtcraftSeedream4p5(PlanArtcraftSeedream4p5 {
|
||||
prompt: request.prompt,
|
||||
image_inputs,
|
||||
image_size,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
@@ -147,7 +148,6 @@ mod tests {
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_seedream_4p5_image_request;
|
||||
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::{
|
||||
BytedanceSeedreamV4p5MultiFunctionImageGenImageSize as S,
|
||||
|
||||
+4
-4
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::{
|
||||
BytedanceSeedream5LiteMultiFunctionImageGenImageSize,
|
||||
BytedanceSeedream5LiteMultiFunctionImageGenNumImages,
|
||||
@@ -22,7 +23,7 @@ pub struct PlanArtcraftSeedream5Lite<'a> {
|
||||
|
||||
pub fn plan_generate_image_artcraft_seedream_5_lite<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftSeedream5Lite<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
@@ -30,13 +31,13 @@ pub fn plan_generate_image_artcraft_seedream_5_lite<'a>(
|
||||
let image_size = plan_image_size(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftSeedream5Lite {
|
||||
Ok(ImageGenerationPlan::ArtcraftSeedream5Lite(PlanArtcraftSeedream5Lite {
|
||||
prompt: request.prompt,
|
||||
image_inputs,
|
||||
image_size,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
@@ -148,7 +149,6 @@ mod tests {
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_seedream_5_lite_image_request;
|
||||
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::{
|
||||
BytedanceSeedream5LiteMultiFunctionImageGenImageSize as S,
|
||||
|
||||
+8
-4
@@ -5,6 +5,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::EnqueueNanoBananaProEditImageAspectRatio;
|
||||
use fal_client::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_image_webhook::EnqueueNanoBananaProTextToImageAspectRatio;
|
||||
|
||||
@@ -44,7 +45,7 @@ pub struct PlanFalNanaBananaPro<'a> {
|
||||
|
||||
pub fn plan_generate_image_fal_nano_banana_pro<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanFalNanaBananaPro<'a>, ArtcraftRouterError> {
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
let image_urls = resolve_image_list_ref(request.image_inputs)?;
|
||||
@@ -53,14 +54,14 @@ pub fn plan_generate_image_fal_nano_banana_pro<'a>(
|
||||
let resolution = plan_resolution(request.resolution)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanFalNanaBananaPro {
|
||||
Ok(ImageGenerationPlan::FalNanaBananaPro(PlanFalNanaBananaPro {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
t2i_aspect_ratio,
|
||||
edit_aspect_ratio,
|
||||
resolution,
|
||||
num_images,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref(
|
||||
@@ -263,7 +264,10 @@ mod tests {
|
||||
fn build_plan<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> PlanFalNanaBananaPro<'a> {
|
||||
plan_generate_image_fal_nano_banana_pro(request).expect("plan should succeed")
|
||||
let ImageGenerationPlan::FalNanaBananaPro(plan) = plan_generate_image_fal_nano_banana_pro(request).expect("plan should succeed") else {
|
||||
panic!("expected FalNanaBananaPro variant")
|
||||
};
|
||||
plan
|
||||
}
|
||||
|
||||
// ── Image inputs / mode detection ────────────────────────────────────────────
|
||||
|
||||
+15
-11
@@ -28,20 +28,24 @@ 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)
|
||||
)),
|
||||
Provider::Artcraft => self.build_artcraft(),
|
||||
_ => self.unsupported_provider(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_artcraft(&self) -> Result<SplatGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.model {
|
||||
CommonSplatModel::Marble0p1Mini => plan_generate_splat_artcraft_marble_0p1_mini(self),
|
||||
CommonSplatModel::Marble0p1Plus => plan_generate_splat_artcraft_marble_0p1_plus(self),
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_provider(&self) -> Result<SplatGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
Err(ArtcraftRouterError::UnsupportedModel(
|
||||
format!("Splat generation for model `{:?}` is not supported for provider {:?}", self.model, self.provider)
|
||||
))
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
+4
-3
@@ -2,6 +2,7 @@ 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 crate::generate::generate_splat::splat_generation_plan::SplatGenerationPlan;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -13,14 +14,14 @@ pub struct PlanArtcraftMarble0p1Mini<'a> {
|
||||
|
||||
pub fn plan_generate_splat_artcraft_marble_0p1_mini<'a>(
|
||||
request: &'a GenerateSplatRequest<'a>,
|
||||
) -> Result<PlanArtcraftMarble0p1Mini<'a>, ArtcraftRouterError> {
|
||||
) -> Result<SplatGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let reference_image = resolve_single_image_ref(request.reference_images)?;
|
||||
|
||||
Ok(PlanArtcraftMarble0p1Mini {
|
||||
Ok(SplatGenerationPlan::ArtcraftMarble0p1Mini(PlanArtcraftMarble0p1Mini {
|
||||
prompt: request.prompt,
|
||||
reference_image,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_single_image_ref<'a>(
|
||||
|
||||
+4
-3
@@ -2,6 +2,7 @@ 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 crate::generate::generate_splat::splat_generation_plan::SplatGenerationPlan;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -13,14 +14,14 @@ pub struct PlanArtcraftMarble0p1Plus<'a> {
|
||||
|
||||
pub fn plan_generate_splat_artcraft_marble_0p1_plus<'a>(
|
||||
request: &'a GenerateSplatRequest<'a>,
|
||||
) -> Result<PlanArtcraftMarble0p1Plus<'a>, ArtcraftRouterError> {
|
||||
) -> Result<SplatGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let reference_image = resolve_single_image_ref(request.reference_images)?;
|
||||
|
||||
Ok(PlanArtcraftMarble0p1Plus {
|
||||
Ok(SplatGenerationPlan::ArtcraftMarble0p1Plus(PlanArtcraftMarble0p1Plus {
|
||||
prompt: request.prompt,
|
||||
reference_image,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_single_image_ref<'a>(
|
||||
|
||||
+2
@@ -69,6 +69,8 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
+2
@@ -69,6 +69,8 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
+2
@@ -72,6 +72,8 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
+5
@@ -31,6 +31,9 @@ pub (crate) fn estimate_video_cost_artcraft_seedance2p0(
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
use_face_blur_hack: None,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = args.estimate_cost_in_usd_cents();
|
||||
@@ -61,6 +64,8 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
pub (crate) mod artcraft;
|
||||
pub (crate) mod muapi;
|
||||
pub (crate) mod seedance2pro;
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
use crate::generate::generate_video::plan::muapi::plan_generate_video_muapi_seedance2p0::{
|
||||
MuapiSeedance2p0Mode, PlanMuapiSeedance2p0,
|
||||
};
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use muapi_client::creds::muapi_api_key::MuapiApiKey;
|
||||
use muapi_client::creds::muapi_session::MuapiSession;
|
||||
use muapi_client::requests::seedance_2p0_image_to_video::seedance_2p0_image_to_video::Seedance2p0ImageToVideoArgs;
|
||||
use muapi_client::requests::seedance_2p0_text_to_video::seedance_2p0_text_to_video::Seedance2p0TextToVideoArgs;
|
||||
|
||||
pub(crate) fn estimate_video_cost_muapi_seedance2p0(
|
||||
plan: &PlanMuapiSeedance2p0,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
// A dummy session is sufficient — cost estimation does not make any network calls.
|
||||
let dummy_session = MuapiSession::new(MuapiApiKey::new(String::new()));
|
||||
|
||||
let cost_in_usd_cents;
|
||||
let cost_in_credits;
|
||||
|
||||
match &plan.mode {
|
||||
MuapiSeedance2p0Mode::TextToVideo { aspect_ratio, duration, quality } => {
|
||||
let args = Seedance2p0TextToVideoArgs {
|
||||
session: &dummy_session,
|
||||
prompt: String::new(),
|
||||
aspect_ratio: *aspect_ratio,
|
||||
duration: *duration,
|
||||
quality: *quality,
|
||||
};
|
||||
|
||||
cost_in_usd_cents = args.estimate_cost_in_usd_cents();
|
||||
|
||||
// NB: This is off by a factor of 100 because they use a fractional system.
|
||||
cost_in_credits = (args.estimate_credits() * 100.0f64).round() as u64;
|
||||
}
|
||||
MuapiSeedance2p0Mode::ImageToVideo { aspect_ratio, duration, quality, .. } => {
|
||||
let args = Seedance2p0ImageToVideoArgs {
|
||||
session: &dummy_session,
|
||||
prompt: String::new(),
|
||||
image_urls: vec![],
|
||||
aspect_ratio: *aspect_ratio,
|
||||
duration: *duration,
|
||||
quality: *quality,
|
||||
};
|
||||
|
||||
cost_in_usd_cents = args.estimate_cost_in_usd_cents();
|
||||
|
||||
// NB: This is off by a factor of 100 because they use a fractional system.
|
||||
cost_in_credits = (args.estimate_credits() * 100.0f64).round() as u64;
|
||||
}
|
||||
}
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_credits),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod estimate_video_cost_muapi_seedance2p0;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
use seedance2pro::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro::requests::generate_video::generate_video::{GenerateVideoArgs, Resolution};
|
||||
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::PlanSeedance2proSeedance2p0;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
pub(crate) fn estimate_video_cost_seedance2pro_seedance2p0(
|
||||
plan: &PlanSeedance2proSeedance2p0,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
// A dummy session is sufficient — cost estimation does not make any network calls.
|
||||
let dummy_session = Seedance2ProSession::from_cookies_string(String::new());
|
||||
|
||||
let args = GenerateVideoArgs {
|
||||
session: &dummy_session,
|
||||
prompt: String::new(),
|
||||
resolution: Resolution::Square1x1, // Resolution does not affect cost
|
||||
duration_seconds: plan.duration_seconds,
|
||||
batch_count: plan.batch_count,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
use_face_blur_hack: None,
|
||||
};
|
||||
|
||||
let cost_in_credits = args.estimate_credits();
|
||||
let cost_in_usd_cents = args.estimate_cost_in_usd_cents();
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_credits as u64),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod estimate_video_cost_seedance2pro_seedance2p0;
|
||||
+2
@@ -18,6 +18,8 @@ pub async fn execute_artcraft_seedance2p0(
|
||||
start_frame_media_token: plan.start_frame.map(|t| t.to_owned()),
|
||||
end_frame_media_token: plan.end_frame.map(|t| t.to_owned()),
|
||||
reference_image_media_tokens: plan.reference_images.map(|tokens| tokens.to_owned()),
|
||||
reference_video_media_tokens: plan.reference_videos.map(|tokens| tokens.to_owned()),
|
||||
reference_audio_media_tokens: plan.reference_audio.map(|tokens| tokens.to_owned()),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
duration_seconds: plan.duration_seconds,
|
||||
batch_count: Some(plan.batch_count),
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
pub mod artcraft;
|
||||
pub mod muapi;
|
||||
pub mod seedance2pro;
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
use crate::client::router_muapi_client::RouterMuapiClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
GenerateVideoResponse, MuapiVideoResponsePayload,
|
||||
};
|
||||
use crate::generate::generate_video::plan::muapi::plan_generate_video_muapi_seedance2p0::{
|
||||
MuapiSeedance2p0Mode, PlanMuapiSeedance2p0,
|
||||
};
|
||||
use muapi_client::requests::seedance_2p0_image_to_video::seedance_2p0_image_to_video::{
|
||||
Seedance2p0ImageToVideoArgs, seedance_2p0_image_to_video,
|
||||
};
|
||||
use muapi_client::requests::seedance_2p0_text_to_video::seedance_2p0_text_to_video::{
|
||||
Seedance2p0TextToVideoArgs, seedance_2p0_text_to_video,
|
||||
};
|
||||
|
||||
pub async fn execute_muapi_seedance2p0(
|
||||
plan: &PlanMuapiSeedance2p0,
|
||||
muapi_client: &RouterMuapiClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let session = &muapi_client.session;
|
||||
|
||||
let request_id = match &plan.mode {
|
||||
MuapiSeedance2p0Mode::TextToVideo { aspect_ratio, duration, quality } => {
|
||||
let args = Seedance2p0TextToVideoArgs {
|
||||
session,
|
||||
prompt: plan.prompt.clone(),
|
||||
aspect_ratio: *aspect_ratio,
|
||||
duration: *duration,
|
||||
quality: *quality,
|
||||
};
|
||||
let response = seedance_2p0_text_to_video(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Muapi(err)))?;
|
||||
response.request_id
|
||||
}
|
||||
MuapiSeedance2p0Mode::ImageToVideo { image_urls, aspect_ratio, duration, quality } => {
|
||||
let args = Seedance2p0ImageToVideoArgs {
|
||||
session,
|
||||
prompt: plan.prompt.clone(),
|
||||
image_urls: image_urls.clone(),
|
||||
aspect_ratio: *aspect_ratio,
|
||||
duration: *duration,
|
||||
quality: *quality,
|
||||
};
|
||||
let response = seedance_2p0_image_to_video(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Muapi(err)))?;
|
||||
response.request_id
|
||||
}
|
||||
};
|
||||
|
||||
Ok(GenerateVideoResponse::Muapi(MuapiVideoResponsePayload {
|
||||
request_id,
|
||||
}))
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod execute_muapi_seedance2p0;
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
GenerateVideoResponse, Seedance2proVideoResponsePayload,
|
||||
};
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::PlanSeedance2proSeedance2p0;
|
||||
use crate::utils::download_file::download_file;
|
||||
use seedance2pro::requests::generate_video::generate_video::{
|
||||
generate_video, GenerateVideoArgs,
|
||||
};
|
||||
use seedance2pro::requests::prepare_file_upload::prepare_file_upload::{
|
||||
prepare_file_upload, PrepareFileUploadArgs,
|
||||
};
|
||||
use seedance2pro::requests::upload_file::upload_file::{
|
||||
upload_file, UploadFileArgs,
|
||||
};
|
||||
use url_utils::extension::extract_extension_from_url::{
|
||||
extract_extension_from_url_str, ExtractExtensions,
|
||||
};
|
||||
|
||||
pub async fn execute_seedance2pro_seedance2p0(
|
||||
plan: &PlanSeedance2proSeedance2p0,
|
||||
seedance2pro_client: &RouterSeedance2ProClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let session = &seedance2pro_client.session;
|
||||
|
||||
// Upload media files to seedance2pro CDN
|
||||
let start_frame_url = upload_optional_url(session, plan.start_frame_url.as_deref()).await?;
|
||||
let end_frame_url = upload_optional_url(session, plan.end_frame_url.as_deref()).await?;
|
||||
let reference_image_urls = upload_optional_url_list(session, plan.reference_image_urls.as_deref()).await?;
|
||||
let reference_video_urls = upload_optional_url_list(session, plan.reference_video_urls.as_deref()).await?;
|
||||
let reference_audio_urls = upload_optional_url_list(session, plan.reference_audio_urls.as_deref()).await?;
|
||||
|
||||
let args = GenerateVideoArgs {
|
||||
session,
|
||||
prompt: plan.prompt.clone().unwrap_or_default(),
|
||||
resolution: plan.resolution,
|
||||
duration_seconds: plan.duration_seconds,
|
||||
batch_count: plan.batch_count,
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
use_face_blur_hack: None,
|
||||
};
|
||||
|
||||
let response = generate_video(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Downloads a file from a source URL and re-uploads it to seedance2pro CDN.
|
||||
async fn upload_to_seedance2pro(
|
||||
session: &seedance2pro::creds::seedance2pro_session::Seedance2ProSession,
|
||||
source_url: &str,
|
||||
) -> Result<String, ArtcraftRouterError> {
|
||||
let extension = extract_extension_from_url_str(source_url, &ExtractExtensions::All)
|
||||
.map(|ext| ext.without_period().to_string())
|
||||
.unwrap_or_else(|| "jpg".to_string());
|
||||
|
||||
let file_bytes = download_file(source_url).await?;
|
||||
|
||||
let prepare_response = prepare_file_upload(PrepareFileUploadArgs {
|
||||
session,
|
||||
extension,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
let upload_response = upload_file(UploadFileArgs {
|
||||
upload_url: prepare_response.upload_url,
|
||||
file_bytes,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(upload_response.public_url)
|
||||
}
|
||||
|
||||
async fn upload_optional_url(
|
||||
session: &seedance2pro::creds::seedance2pro_session::Seedance2ProSession,
|
||||
url: Option<&str>,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
match url {
|
||||
None => Ok(None),
|
||||
Some(u) => Ok(Some(upload_to_seedance2pro(session, u).await?)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn upload_optional_url_list(
|
||||
session: &seedance2pro::creds::seedance2pro_session::Seedance2ProSession,
|
||||
urls: Option<&[String]>,
|
||||
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
|
||||
match urls {
|
||||
None => Ok(None),
|
||||
Some(url_list) if url_list.is_empty() => Ok(None),
|
||||
Some(url_list) => {
|
||||
let mut uploaded = Vec::with_capacity(url_list.len());
|
||||
for url in url_list {
|
||||
uploaded.push(upload_to_seedance2pro(session, url).await?);
|
||||
}
|
||||
Ok(Some(uploaded))
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod execute_seedance2pro_seedance2p0;
|
||||
+47
-18
@@ -1,17 +1,24 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
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::plan::muapi::plan_generate_video_muapi_seedance2p0::plan_generate_video_muapi_seedance2p0;
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::plan_generate_video_seedance2pro_seedance2p0;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
/// Plan to either (1) generate a video or (2) determine how much it costs to generate that video.
|
||||
/// This works across multiple providers by shaping a generic "GenerateVideoRequest" into a provider-specific plan.
|
||||
/// That plan can then be used to return a cost estimate for that given provider or return a struct that can be used to send a real generation request.
|
||||
pub struct GenerateVideoRequest<'a> {
|
||||
/// Which model to use.
|
||||
pub model: CommonVideoModel,
|
||||
@@ -34,6 +41,12 @@ pub struct GenerateVideoRequest<'a> {
|
||||
/// Reference images (optional).
|
||||
pub reference_images: Option<ImageListRef<'a>>,
|
||||
|
||||
/// Reference videos (optional).
|
||||
pub reference_videos: Option<VideoListRef<'a>>,
|
||||
|
||||
/// Reference audio (optional).
|
||||
pub reference_audio: Option<AudioListRef<'a>>,
|
||||
|
||||
/// The resolution to use
|
||||
pub resolution: Option<CommonResolution>,
|
||||
|
||||
@@ -64,27 +77,43 @@ impl<'a> GenerateVideoRequest<'a> {
|
||||
/// Read the video generation request, construct a plan, then yield a means to execute it.
|
||||
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)
|
||||
}
|
||||
_ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))),
|
||||
},
|
||||
Provider::Fal => Err(ArtcraftRouterError::UnsupportedModel(
|
||||
format!("Video generation via Fal is not yet supported (model: {:?})", self.model)
|
||||
)),
|
||||
Provider::Artcraft => self.build_artcraft(),
|
||||
Provider::Muapi => self.build_muapi(),
|
||||
Provider::Seedance2Pro => self.build_seedance2pro(),
|
||||
_ => self.unsupported_provider(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_artcraft(&self) -> Result<VideoGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.model {
|
||||
CommonVideoModel::Kling3p0Pro => plan_generate_video_artcraft_kling3p0_pro(self),
|
||||
CommonVideoModel::Kling3p0Standard => plan_generate_video_artcraft_kling3p0_standard(self),
|
||||
CommonVideoModel::Seedance1p5Pro => plan_generate_video_artcraft_seedance1p5_pro(self),
|
||||
CommonVideoModel::Seedance2p0 => plan_generate_video_artcraft_seedance2p0(self),
|
||||
_ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_muapi(&self) -> Result<VideoGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.model {
|
||||
CommonVideoModel::Seedance2p0 => plan_generate_video_muapi_seedance2p0(self),
|
||||
_ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_seedance2pro(&self) -> Result<VideoGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.model {
|
||||
CommonVideoModel::Seedance2p0 => plan_generate_video_seedance2pro_seedance2p0(self),
|
||||
_ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))),
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_provider(&self) -> Result<VideoGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
Err(ArtcraftRouterError::UnsupportedModel(
|
||||
format!("Video generation for model `{:?}` is not supported for provider {:?}", self.model, self.provider)
|
||||
))
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
+29
@@ -1,3 +1,4 @@
|
||||
use muapi_client::api_types::request_id::RequestId as MuapiRequestId;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -6,15 +7,43 @@ pub struct ArtcraftVideoResponsePayload {
|
||||
pub all_inference_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MuapiVideoResponsePayload {
|
||||
pub request_id: MuapiRequestId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Seedance2proVideoResponsePayload {
|
||||
pub order_id: String,
|
||||
pub task_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum GenerateVideoResponse {
|
||||
Artcraft(ArtcraftVideoResponsePayload),
|
||||
Muapi(MuapiVideoResponsePayload),
|
||||
Seedance2Pro(Seedance2proVideoResponsePayload),
|
||||
}
|
||||
|
||||
impl GenerateVideoResponse {
|
||||
pub fn get_artcraft_payload(&self) -> Option<ArtcraftVideoResponsePayload> {
|
||||
match self {
|
||||
Self::Artcraft(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_muapi_payload(&self) -> Option<MuapiVideoResponsePayload> {
|
||||
match self {
|
||||
Self::Muapi(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proVideoResponsePayload> {
|
||||
match self {
|
||||
Self::Seedance2Pro(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::{
|
||||
Kling3p0ProMultiFunctionVideoGenAspectRatio,
|
||||
Kling3p0ProMultiFunctionVideoGenDuration,
|
||||
@@ -24,7 +25,7 @@ pub struct PlanArtcraftKling3p0Pro<'a> {
|
||||
|
||||
pub fn plan_generate_video_artcraft_kling3p0_pro<'a>(
|
||||
request: &'a GenerateVideoRequest<'a>,
|
||||
) -> Result<PlanArtcraftKling3p0Pro<'a>, ArtcraftRouterError> {
|
||||
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame = resolve_image_ref(request.start_frame)?;
|
||||
@@ -33,7 +34,7 @@ pub fn plan_generate_video_artcraft_kling3p0_pro<'a>(
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftKling3p0Pro {
|
||||
Ok(VideoGenerationPlan::ArtcraftKling3p0Pro(PlanArtcraftKling3p0Pro {
|
||||
prompt: request.prompt,
|
||||
negative_prompt: request.negative_prompt,
|
||||
start_frame,
|
||||
@@ -42,7 +43,7 @@ pub fn plan_generate_video_artcraft_kling3p0_pro<'a>(
|
||||
duration,
|
||||
generate_audio: request.generate_audio,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_ref<'a>(
|
||||
@@ -177,6 +178,8 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
|
||||
+6
-3
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::{
|
||||
Kling3p0StandardMultiFunctionVideoGenAspectRatio,
|
||||
Kling3p0StandardMultiFunctionVideoGenDuration,
|
||||
@@ -24,7 +25,7 @@ pub struct PlanArtcraftKling3p0Standard<'a> {
|
||||
|
||||
pub fn plan_generate_video_artcraft_kling3p0_standard<'a>(
|
||||
request: &'a GenerateVideoRequest<'a>,
|
||||
) -> Result<PlanArtcraftKling3p0Standard<'a>, ArtcraftRouterError> {
|
||||
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame = resolve_image_ref(request.start_frame)?;
|
||||
@@ -33,7 +34,7 @@ pub fn plan_generate_video_artcraft_kling3p0_standard<'a>(
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftKling3p0Standard {
|
||||
Ok(VideoGenerationPlan::ArtcraftKling3p0Standard(PlanArtcraftKling3p0Standard {
|
||||
prompt: request.prompt,
|
||||
negative_prompt: request.negative_prompt,
|
||||
start_frame,
|
||||
@@ -42,7 +43,7 @@ pub fn plan_generate_video_artcraft_kling3p0_standard<'a>(
|
||||
duration,
|
||||
generate_audio: request.generate_audio,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_ref<'a>(
|
||||
@@ -177,6 +178,8 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
|
||||
+6
-3
@@ -4,6 +4,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::{
|
||||
Seedance1p5ProMultiFunctionVideoGenAspectRatio,
|
||||
Seedance1p5ProMultiFunctionVideoGenDuration,
|
||||
@@ -25,7 +26,7 @@ pub struct PlanArtcraftSeedance1p5Pro<'a> {
|
||||
|
||||
pub fn plan_generate_video_artcraft_seedance1p5_pro<'a>(
|
||||
request: &'a GenerateVideoRequest<'a>,
|
||||
) -> Result<PlanArtcraftSeedance1p5Pro<'a>, ArtcraftRouterError> {
|
||||
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame = resolve_image_ref(request.start_frame)?;
|
||||
@@ -35,7 +36,7 @@ pub fn plan_generate_video_artcraft_seedance1p5_pro<'a>(
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftSeedance1p5Pro {
|
||||
Ok(VideoGenerationPlan::ArtcraftSeedance1p5Pro(PlanArtcraftSeedance1p5Pro {
|
||||
prompt: request.prompt,
|
||||
start_frame,
|
||||
end_frame,
|
||||
@@ -44,7 +45,7 @@ pub fn plan_generate_video_artcraft_seedance1p5_pro<'a>(
|
||||
resolution,
|
||||
generate_audio: request.generate_audio,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_ref<'a>(
|
||||
@@ -190,6 +191,8 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
|
||||
+36
-3
@@ -1,10 +1,13 @@
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
|
||||
Seedance2p0AspectRatio, Seedance2p0BatchCount,
|
||||
};
|
||||
@@ -16,6 +19,8 @@ pub struct PlanArtcraftSeedance2p0<'a> {
|
||||
pub start_frame: Option<&'a MediaFileToken>,
|
||||
pub end_frame: Option<&'a MediaFileToken>,
|
||||
pub reference_images: Option<&'a Vec<MediaFileToken>>,
|
||||
pub reference_videos: Option<&'a Vec<MediaFileToken>>,
|
||||
pub reference_audio: Option<&'a Vec<MediaFileToken>>,
|
||||
pub aspect_ratio: Option<Seedance2p0AspectRatio>,
|
||||
pub duration_seconds: Option<u8>,
|
||||
pub batch_count: Seedance2p0BatchCount,
|
||||
@@ -24,27 +29,31 @@ pub struct PlanArtcraftSeedance2p0<'a> {
|
||||
|
||||
pub fn plan_generate_video_artcraft_seedance2p0<'a>(
|
||||
request: &'a GenerateVideoRequest<'a>,
|
||||
) -> Result<PlanArtcraftSeedance2p0<'a>, ArtcraftRouterError> {
|
||||
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame = resolve_image_ref(request.start_frame)?;
|
||||
let end_frame = resolve_image_ref(request.end_frame)?;
|
||||
let reference_images = resolve_image_list_ref(request.reference_images)?;
|
||||
let reference_videos = resolve_video_list_ref(request.reference_videos)?;
|
||||
let reference_audio = resolve_audio_list_ref(request.reference_audio)?;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
|
||||
let duration_seconds = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftSeedance2p0 {
|
||||
Ok(VideoGenerationPlan::ArtcraftSeedance2p0(PlanArtcraftSeedance2p0 {
|
||||
prompt: request.prompt,
|
||||
start_frame,
|
||||
end_frame,
|
||||
reference_images,
|
||||
reference_videos,
|
||||
reference_audio,
|
||||
aspect_ratio,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_ref<'a>(
|
||||
@@ -71,6 +80,30 @@ fn resolve_image_list_ref<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_video_list_ref<'a>(
|
||||
video_list_ref: Option<VideoListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match video_list_ref {
|
||||
None => Ok(None),
|
||||
Some(VideoListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(VideoListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_audio_list_ref<'a>(
|
||||
audio_list_ref: Option<AudioListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match audio_list_ref {
|
||||
None => Ok(None),
|
||||
Some(AudioListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(AudioListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Supported aspect ratios and their AR values (width / height):
|
||||
// Portrait9x16 = 0.5625, Portrait3x4 = 0.75, Square1x1 = 1.0, Standard4x3 = 1.33, Landscape16x9 = 1.78
|
||||
//
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
pub mod artcraft;
|
||||
pub mod artcraft;
|
||||
pub mod muapi;
|
||||
pub mod seedance2pro;
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod plan_generate_video_muapi_seedance2p0;
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
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_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use muapi_client::requests::seedance_2p0_image_to_video::seedance_2p0_image_to_video::{
|
||||
AspectRatio as I2vAspectRatio,
|
||||
Duration as I2vDuration,
|
||||
Quality as I2vQuality,
|
||||
};
|
||||
use muapi_client::requests::seedance_2p0_text_to_video::seedance_2p0_text_to_video::{
|
||||
AspectRatio as T2vAspectRatio,
|
||||
Duration as T2vDuration,
|
||||
Quality as T2vQuality,
|
||||
};
|
||||
|
||||
/// The generation mode determined at plan time.
|
||||
/// Muapi has separate text-to-video and image-to-video endpoints.
|
||||
/// If reference images are present, we use image-to-video; otherwise text-to-video.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MuapiSeedance2p0Mode {
|
||||
TextToVideo {
|
||||
aspect_ratio: T2vAspectRatio,
|
||||
duration: T2vDuration,
|
||||
quality: T2vQuality,
|
||||
},
|
||||
ImageToVideo {
|
||||
image_urls: Vec<String>,
|
||||
aspect_ratio: I2vAspectRatio,
|
||||
duration: I2vDuration,
|
||||
quality: I2vQuality,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanMuapiSeedance2p0 {
|
||||
pub prompt: String,
|
||||
pub mode: MuapiSeedance2p0Mode,
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_muapi_seedance2p0<'a>(
|
||||
request: &'a GenerateVideoRequest<'a>,
|
||||
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Muapi does not support start/end frames, video references, or audio references.
|
||||
if request.start_frame.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "start_frame",
|
||||
value: "Muapi does not support start frame keyframes".to_string(),
|
||||
}));
|
||||
}
|
||||
if request.end_frame.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "end_frame",
|
||||
value: "Muapi does not support end frame keyframes".to_string(),
|
||||
}));
|
||||
}
|
||||
if request.reference_videos.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "reference_videos",
|
||||
value: "Muapi does not support video references".to_string(),
|
||||
}));
|
||||
}
|
||||
if request.reference_audio.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "reference_audio",
|
||||
value: "Muapi does not support audio references".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
let image_urls = resolve_image_urls(request.reference_images)?;
|
||||
|
||||
let prompt = request.prompt.unwrap_or("").to_string();
|
||||
|
||||
let mode = match image_urls {
|
||||
Some(urls) => {
|
||||
MuapiSeedance2p0Mode::ImageToVideo {
|
||||
image_urls: urls,
|
||||
aspect_ratio: plan_i2v_aspect_ratio(request.aspect_ratio, strategy)?,
|
||||
duration: plan_i2v_duration(request.duration_seconds, strategy)?,
|
||||
quality: I2vQuality::High, // NB: "Low" is a "fast" mode, I think
|
||||
}
|
||||
}
|
||||
None => {
|
||||
MuapiSeedance2p0Mode::TextToVideo {
|
||||
aspect_ratio: plan_t2v_aspect_ratio(request.aspect_ratio, strategy)?,
|
||||
duration: plan_t2v_duration(request.duration_seconds, strategy)?,
|
||||
quality: T2vQuality::High, // NB: "Low" is a "fast" mode, I think
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(VideoGenerationPlan::MuapiSeedance2p0(PlanMuapiSeedance2p0 {
|
||||
prompt,
|
||||
mode,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_urls(
|
||||
reference_images: Option<ImageListRef<'_>>,
|
||||
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
|
||||
match reference_images {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::Urls(urls)) => {
|
||||
if urls.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(urls.clone()))
|
||||
}
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::MuapiOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Muapi supports: 16:9, 9:16, 4:3, 3:4 (no square, no 1:1)
|
||||
fn plan_t2v_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<T2vAspectRatio, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(T2vAspectRatio::Landscape16x9),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(T2vAspectRatio::Landscape16x9)
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(T2vAspectRatio::Portrait9x16)
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(T2vAspectRatio::Standard4x3),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(T2vAspectRatio::Portrait3x4),
|
||||
|
||||
// Square and others are not supported by Muapi
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(nearest_t2v_aspect_ratio(unsupported))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_t2v_aspect_ratio(aspect_ratio: CommonAspectRatio) -> T2vAspectRatio {
|
||||
match aspect_ratio {
|
||||
// Wide ratios → nearest wide
|
||||
CommonAspectRatio::Square | CommonAspectRatio::SquareHd
|
||||
| CommonAspectRatio::WideFiveByFour => T2vAspectRatio::Standard4x3,
|
||||
CommonAspectRatio::WideThreeByTwo => T2vAspectRatio::Landscape16x9,
|
||||
CommonAspectRatio::WideTwentyOneByNine => T2vAspectRatio::Landscape16x9,
|
||||
// Tall ratios → nearest tall
|
||||
CommonAspectRatio::TallFourByFive => T2vAspectRatio::Portrait3x4,
|
||||
CommonAspectRatio::TallTwoByThree => T2vAspectRatio::Portrait9x16,
|
||||
CommonAspectRatio::TallNineByTwentyOne => T2vAspectRatio::Portrait9x16,
|
||||
_ => T2vAspectRatio::Landscape16x9,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_i2v_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<I2vAspectRatio, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(I2vAspectRatio::Landscape16x9),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(I2vAspectRatio::Landscape16x9)
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(I2vAspectRatio::Portrait9x16)
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(I2vAspectRatio::Standard4x3),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(I2vAspectRatio::Portrait3x4),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(nearest_i2v_aspect_ratio(unsupported))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_i2v_aspect_ratio(aspect_ratio: CommonAspectRatio) -> I2vAspectRatio {
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::Square | CommonAspectRatio::SquareHd
|
||||
| CommonAspectRatio::WideFiveByFour => I2vAspectRatio::Standard4x3,
|
||||
CommonAspectRatio::WideThreeByTwo => I2vAspectRatio::Landscape16x9,
|
||||
CommonAspectRatio::WideTwentyOneByNine => I2vAspectRatio::Landscape16x9,
|
||||
CommonAspectRatio::TallFourByFive => I2vAspectRatio::Portrait3x4,
|
||||
CommonAspectRatio::TallTwoByThree => I2vAspectRatio::Portrait9x16,
|
||||
CommonAspectRatio::TallNineByTwentyOne => I2vAspectRatio::Portrait9x16,
|
||||
_ => I2vAspectRatio::Landscape16x9,
|
||||
}
|
||||
}
|
||||
|
||||
// Muapi supports durations of 5, 10, and 15 seconds.
|
||||
fn plan_t2v_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<T2vDuration, ArtcraftRouterError> {
|
||||
let d = duration_seconds.unwrap_or(5);
|
||||
match d {
|
||||
0..=5 => Ok(T2vDuration::FiveSeconds),
|
||||
6..=10 => Ok(T2vDuration::TenSeconds),
|
||||
11..=15 => Ok(T2vDuration::FifteenSeconds),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(T2vDuration::FifteenSeconds), // clamp to max
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_i2v_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<I2vDuration, ArtcraftRouterError> {
|
||||
let d = duration_seconds.unwrap_or(5);
|
||||
match d {
|
||||
0..=5 => Ok(I2vDuration::FiveSeconds),
|
||||
6..=10 => Ok(I2vDuration::TenSeconds),
|
||||
11..=15 => Ok(I2vDuration::FifteenSeconds),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(I2vDuration::FifteenSeconds),
|
||||
},
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod plan_generate_video_seedance2pro_seedance2p0;
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use seedance2pro::requests::generate_video::generate_video::{BatchCount, Resolution};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanSeedance2proSeedance2p0 {
|
||||
pub prompt: Option<String>,
|
||||
pub start_frame_url: Option<String>,
|
||||
pub end_frame_url: Option<String>,
|
||||
pub reference_image_urls: Option<Vec<String>>,
|
||||
pub reference_video_urls: Option<Vec<String>>,
|
||||
pub reference_audio_urls: Option<Vec<String>>,
|
||||
pub resolution: Resolution,
|
||||
pub duration_seconds: u8,
|
||||
pub batch_count: BatchCount,
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_seedance2pro_seedance2p0<'a>(
|
||||
request: &'a GenerateVideoRequest<'a>,
|
||||
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame_url = resolve_image_ref_url(request.start_frame)?;
|
||||
let end_frame_url = resolve_image_ref_url(request.end_frame)?;
|
||||
let reference_image_urls = resolve_image_list_ref_urls(request.reference_images)?;
|
||||
let reference_video_urls = resolve_video_list_ref_urls(request.reference_videos)?;
|
||||
let reference_audio_urls = resolve_audio_list_ref_urls(request.reference_audio)?;
|
||||
|
||||
let resolution = plan_resolution(request.aspect_ratio, strategy)?;
|
||||
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
|
||||
let duration_seconds = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(VideoGenerationPlan::Seedance2proSeedance2p0(PlanSeedance2proSeedance2p0 {
|
||||
prompt: request.prompt.map(|p| p.to_string()),
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
resolution,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_ref_url(
|
||||
image_ref: Option<ImageRef<'_>>,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
match image_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageRef::Url(url)) => Ok(Some(url.to_string())),
|
||||
Some(ImageRef::MediaFileToken(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::Seedance2ProOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref_urls(
|
||||
image_list_ref: Option<ImageListRef<'_>>,
|
||||
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(Some(urls.clone())),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::Seedance2ProOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_video_list_ref_urls(
|
||||
video_list_ref: Option<VideoListRef<'_>>,
|
||||
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
|
||||
match video_list_ref {
|
||||
None => Ok(None),
|
||||
Some(VideoListRef::Urls(urls)) => Ok(Some(urls.clone())),
|
||||
Some(VideoListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::Seedance2ProOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_audio_list_ref_urls(
|
||||
audio_list_ref: Option<AudioListRef<'_>>,
|
||||
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
|
||||
match audio_list_ref {
|
||||
None => Ok(None),
|
||||
Some(AudioListRef::Urls(urls)) => Ok(Some(urls.clone())),
|
||||
Some(AudioListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::Seedance2ProOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Supported aspect ratios and their AR values (width / height):
|
||||
// Portrait9x16 = 0.5625, Portrait3x4 = 0.75, Square1x1 = 1.0, Standard4x3 = 1.33, Landscape16x9 = 1.78
|
||||
//
|
||||
// All supported ratios cost the same, so PayMoreUpgrade and PayLessDowngrade both
|
||||
// select the nearest match rather than rounding in a specific direction.
|
||||
fn plan_resolution(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Resolution, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
// No preference or auto — default to landscape
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Resolution::Landscape16x9),
|
||||
|
||||
// Direct mappings
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(Resolution::Landscape16x9)
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(Resolution::Portrait9x16)
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(Resolution::Square1x1)
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Resolution::Standard4x3),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Resolution::Portrait3x4),
|
||||
|
||||
// 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(nearest_resolution(unsupported))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick the nearest supported resolution by AR value (width / height).
|
||||
fn nearest_resolution(aspect_ratio: CommonAspectRatio) -> Resolution {
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => Resolution::Standard4x3, // 1.25, nearest 1.33
|
||||
CommonAspectRatio::WideThreeByTwo => Resolution::Standard4x3, // 1.50, nearest 1.33
|
||||
CommonAspectRatio::WideTwentyOneByNine => Resolution::Landscape16x9, // 2.33, nearest 1.78
|
||||
CommonAspectRatio::TallFourByFive => Resolution::Portrait3x4, // 0.80, nearest 0.75
|
||||
CommonAspectRatio::TallTwoByThree => Resolution::Portrait3x4, // 0.67, nearest 0.75
|
||||
CommonAspectRatio::TallNineByTwentyOne => Resolution::Portrait9x16, // 0.43, nearest 0.56
|
||||
_ => Resolution::Square1x1,
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance2p0 supports batch counts of 1, 2, and 4 only.
|
||||
fn plan_batch_count(
|
||||
video_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<BatchCount, ArtcraftRouterError> {
|
||||
let count = video_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(BatchCount::One),
|
||||
2 => Ok(BatchCount::Two),
|
||||
4 => Ok(BatchCount::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "video_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
Ok(if count < 4 { BatchCount::Four } else { BatchCount::Four })
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(if count < 4 { BatchCount::Two } else { BatchCount::Four })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance2p0 supports duration of 4–15 seconds.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<u8, ArtcraftRouterError> {
|
||||
const MIN: u16 = 4;
|
||||
const MAX: u16 = 15;
|
||||
const DEFAULT: u8 = 5;
|
||||
match duration_seconds {
|
||||
None => Ok(DEFAULT),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(d as u8),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(d.clamp(MIN, MAX) as u8),
|
||||
},
|
||||
}
|
||||
}
|
||||
+22
@@ -4,15 +4,21 @@ use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraf
|
||||
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::cost::muapi::estimate_video_cost_muapi_seedance2p0::estimate_video_cost_muapi_seedance2p0;
|
||||
use crate::generate::generate_video::cost::seedance2pro::estimate_video_cost_seedance2pro_seedance2p0::estimate_video_cost_seedance2pro_seedance2p0;
|
||||
use crate::generate::generate_video::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::execute::muapi::execute_muapi_seedance2p0::execute_muapi_seedance2p0;
|
||||
use crate::generate::generate_video::execute::seedance2pro::execute_seedance2pro_seedance2p0::execute_seedance2pro_seedance2p0;
|
||||
use crate::generate::generate_video::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::plan::muapi::plan_generate_video_muapi_seedance2p0::PlanMuapiSeedance2p0;
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::PlanSeedance2proSeedance2p0;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -21,6 +27,8 @@ pub enum VideoGenerationPlan<'a> {
|
||||
ArtcraftKling3p0Standard(PlanArtcraftKling3p0Standard<'a>),
|
||||
ArtcraftSeedance1p5Pro(PlanArtcraftSeedance1p5Pro<'a>),
|
||||
ArtcraftSeedance2p0(PlanArtcraftSeedance2p0<'a>),
|
||||
MuapiSeedance2p0(PlanMuapiSeedance2p0),
|
||||
Seedance2proSeedance2p0(PlanSeedance2proSeedance2p0),
|
||||
}
|
||||
|
||||
impl<'a> VideoGenerationPlan<'a> {
|
||||
@@ -45,6 +53,14 @@ impl<'a> VideoGenerationPlan<'a> {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_seedance2p0(plan, artcraft_client).await
|
||||
}
|
||||
VideoGenerationPlan::MuapiSeedance2p0(plan) => {
|
||||
let muapi_client = client.get_muapi_client_ref()?;
|
||||
execute_muapi_seedance2p0(plan, muapi_client).await
|
||||
}
|
||||
VideoGenerationPlan::Seedance2proSeedance2p0(plan) => {
|
||||
let seedance2pro_client = client.get_seedance2pro_client_ref()?;
|
||||
execute_seedance2pro_seedance2p0(plan, seedance2pro_client).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +78,12 @@ impl<'a> VideoGenerationPlan<'a> {
|
||||
VideoGenerationPlan::ArtcraftSeedance2p0(plan) => {
|
||||
estimate_video_cost_artcraft_seedance2p0(plan)
|
||||
}
|
||||
VideoGenerationPlan::MuapiSeedance2p0(plan) => {
|
||||
estimate_video_cost_muapi_seedance2p0(plan)
|
||||
}
|
||||
VideoGenerationPlan::Seedance2proSeedance2p0(plan) => {
|
||||
estimate_video_cost_seedance2pro_seedance2p0(plan)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod api;
|
||||
pub mod client;
|
||||
pub mod errors;
|
||||
pub mod generate;
|
||||
pub mod utils;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test_helpers;
|
||||
|
||||
@@ -290,6 +290,8 @@ pub fn base_video_request() -> GenerateVideoRequest<'static> {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::download_error::DownloadError;
|
||||
|
||||
/// Download a file from a URL, returning its bytes.
|
||||
pub async fn download_file(url: &str) -> Result<Vec<u8>, ArtcraftRouterError> {
|
||||
let response = reqwest::get(url)
|
||||
.await
|
||||
.map_err(|err| DownloadError::ReqwestDownload {
|
||||
url: url.to_string(),
|
||||
maybe_status: err.status(),
|
||||
error: err,
|
||||
})?;
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(DownloadError::BadStatus {
|
||||
url: url.to_string(),
|
||||
status_code: status.as_u16(),
|
||||
}.into());
|
||||
}
|
||||
|
||||
response.bytes()
|
||||
.await
|
||||
.map(|b| b.to_vec())
|
||||
.map_err(|err| DownloadError::ReqwestDownload {
|
||||
url: url.to_string(),
|
||||
maybe_status: err.status(),
|
||||
error: err,
|
||||
}.into())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod download_file;
|
||||
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "muapi_client"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
name = "muapi_client"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
workspace-build-acceleration.workspace = true
|
||||
errors.workspace = true
|
||||
chrono.workspace = true
|
||||
cloudflare_errors.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
wreq-util.workspace = true
|
||||
wreq.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
env.workspace = true
|
||||
env_logger.workspace = true
|
||||
test_data = { path = "../../lib/test_data" }
|
||||
testing = { path = "../../lib/testing" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
@@ -0,0 +1 @@
|
||||
pub mod request_id;
|
||||
@@ -0,0 +1,24 @@
|
||||
/// ID for a prediction request in the Muapi API.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RequestId(String);
|
||||
|
||||
impl RequestId {
|
||||
|
||||
pub fn new(value: String) -> Self {
|
||||
RequestId(value)
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Self {
|
||||
RequestId(value.to_string())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RequestId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod muapi_api_key;
|
||||
pub mod muapi_session;
|
||||
@@ -0,0 +1,20 @@
|
||||
/// An API key is the credential required to interact with the Muapi API.
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MuapiApiKey {
|
||||
api_key: String,
|
||||
}
|
||||
|
||||
impl MuapiApiKey {
|
||||
pub fn new(api_key: String) -> Self {
|
||||
MuapiApiKey { api_key }
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.api_key
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
self.api_key.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use crate::creds::muapi_api_key::MuapiApiKey;
|
||||
|
||||
/// Holds the full session data needed to make authenticated requests to Muapi.
|
||||
#[derive(Clone)]
|
||||
pub struct MuapiSession {
|
||||
pub api_key: MuapiApiKey,
|
||||
}
|
||||
|
||||
impl MuapiSession {
|
||||
pub fn new(api_key: MuapiApiKey) -> Self {
|
||||
MuapiSession { api_key }
|
||||
}
|
||||
|
||||
pub fn from_api_key_string(api_key: String) -> Self {
|
||||
MuapiSession {
|
||||
api_key: MuapiApiKey::new(api_key),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod muapi_client_error;
|
||||
pub mod muapi_error;
|
||||
pub mod muapi_generic_api_error;
|
||||
pub mod muapi_specific_api_error;
|
||||
@@ -0,0 +1,26 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MuapiClientError {
|
||||
/// No API key is present in the session.
|
||||
NoApiKeyPresent,
|
||||
|
||||
/// Error parsing a URL.
|
||||
UrlParseError(url::ParseError),
|
||||
|
||||
/// An error was encountered in building the Wreq client.
|
||||
WreqClientError(wreq::Error),
|
||||
}
|
||||
|
||||
impl Error for MuapiClientError {}
|
||||
|
||||
impl Display for MuapiClientError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoApiKeyPresent => write!(f, "No API key present in the session."),
|
||||
Self::UrlParseError(err) => write!(f, "URL parse error: {}", err),
|
||||
Self::WreqClientError(err) => write!(f, "Wreq client error (during client creation): {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use crate::error::muapi_client_error::MuapiClientError;
|
||||
use crate::error::muapi_generic_api_error::MuapiGenericApiError;
|
||||
use crate::error::muapi_specific_api_error::MuapiSpecificApiError;
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MuapiError {
|
||||
Client(MuapiClientError),
|
||||
ApiSpecific(MuapiSpecificApiError),
|
||||
ApiGeneric(MuapiGenericApiError),
|
||||
}
|
||||
|
||||
impl MuapiError {
|
||||
pub fn is_having_downtime_issues(&self) -> bool {
|
||||
match self {
|
||||
Self::ApiGeneric(MuapiGenericApiError::CloudflareError(CloudflareError::BadGateway502)) => true,
|
||||
Self::ApiGeneric(MuapiGenericApiError::CloudflareError(CloudflareError::GatewayTimeout504)) => true,
|
||||
Self::ApiGeneric(MuapiGenericApiError::CloudflareError(CloudflareError::TimeoutOccurred524)) => true,
|
||||
Self::ApiGeneric(MuapiGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body: _ }) => {
|
||||
match status_code.as_u16() {
|
||||
502 => true,
|
||||
504 => true,
|
||||
524 => true,
|
||||
_ => false,
|
||||
}
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for MuapiError {}
|
||||
|
||||
impl std::fmt::Display for MuapiError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Client(e) => write!(f, "MuapiClientError: {:?}", e),
|
||||
Self::ApiSpecific(e) => write!(f, "MuapiSpecificApiError: {:?}", e),
|
||||
Self::ApiGeneric(e) => write!(f, "MuapiGenericApiError: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MuapiClientError> for MuapiError {
|
||||
fn from(error: MuapiClientError) -> Self {
|
||||
Self::Client(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MuapiSpecificApiError> for MuapiError {
|
||||
fn from(error: MuapiSpecificApiError) -> Self {
|
||||
Self::ApiSpecific(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MuapiGenericApiError> for MuapiError {
|
||||
fn from(error: MuapiGenericApiError) -> Self {
|
||||
Self::ApiGeneric(error)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use wreq::StatusCode;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MuapiGenericApiError {
|
||||
/// Specific Cloudflare errors.
|
||||
CloudflareError(CloudflareError),
|
||||
|
||||
/// serde_json::Error, likely from JSON deserialization schema mismatch.
|
||||
/// Includes the original body.
|
||||
SerdeResponseParseErrorWithBody(serde_json::Error, String),
|
||||
|
||||
/// serde_json::Error on a non-200 (error) response.
|
||||
/// Includes the original body.
|
||||
SerdeParseErrorWithBodyOnNon200(serde_json::Error, String),
|
||||
|
||||
/// An uncategorized bad HTTP response with status code and body.
|
||||
UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
},
|
||||
|
||||
/// An uncaught error from the HTTP client.
|
||||
WreqError(wreq::Error),
|
||||
}
|
||||
|
||||
impl Error for MuapiGenericApiError {}
|
||||
|
||||
impl Display for MuapiGenericApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CloudflareError(err) => write!(f, "Cloudflare error: {}", err),
|
||||
Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body),
|
||||
Self::SerdeParseErrorWithBodyOnNon200(err, body) => write!(f, "Failed to parse non-200 response body: {:?}. Body: {}", err, body),
|
||||
Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body),
|
||||
Self::WreqError(err) => write!(f, "Wreq client error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MuapiSpecificApiError {
|
||||
/// Invalid API key.
|
||||
InvalidApiKey,
|
||||
}
|
||||
|
||||
impl Error for MuapiSpecificApiError {}
|
||||
|
||||
impl Display for MuapiSpecificApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidApiKey => write!(f, "Unauthorized: invalid API key."),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#[cfg(test)]
|
||||
pub (crate) mod test_utils;
|
||||
|
||||
pub mod api_types;
|
||||
pub mod creds;
|
||||
pub mod error;
|
||||
pub mod requests;
|
||||
pub mod utils;
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod poll_prediction_result;
|
||||
pub mod seedance_2p0_image_to_video;
|
||||
pub mod seedance_2p0_text_to_video;
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod poll_prediction_result;
|
||||
mod request_types;
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
use crate::api_types::request_id::RequestId;
|
||||
use crate::creds::muapi_session::MuapiSession;
|
||||
use crate::error::muapi_client_error::MuapiClientError;
|
||||
use crate::error::muapi_error::MuapiError;
|
||||
use crate::error::muapi_generic_api_error::MuapiGenericApiError;
|
||||
use crate::requests::poll_prediction_result::request_types::*;
|
||||
use log::info;
|
||||
use wreq::Client;
|
||||
|
||||
const PREDICTIONS_BASE_URL: &str = "https://api.muapi.ai/api/v1/predictions";
|
||||
|
||||
// --- Public types ---
|
||||
|
||||
/// The lifecycle status of a prediction.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum PredictionStatus {
|
||||
/// The task is queued and has not started yet.
|
||||
Pending,
|
||||
/// The task is actively being processed.
|
||||
Processing,
|
||||
/// The task finished successfully. `outputs` will be populated.
|
||||
Completed,
|
||||
/// The task failed. `error` will contain the reason.
|
||||
Failed,
|
||||
/// An unrecognised status string was returned by the server.
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl PredictionStatus {
|
||||
fn from_str(s: &str) -> Self {
|
||||
match s {
|
||||
"pending" => Self::Pending,
|
||||
"processing" => Self::Processing,
|
||||
"completed" => Self::Completed,
|
||||
"failed" => Self::Failed,
|
||||
other => Self::Unknown(other.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_terminal(&self) -> bool {
|
||||
matches!(self, Self::Completed | Self::Failed)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Args & response ---
|
||||
|
||||
pub struct PollPredictionResultArgs<'a> {
|
||||
pub session: &'a MuapiSession,
|
||||
|
||||
/// The request_id returned from a submission endpoint.
|
||||
pub request_id: &'a RequestId,
|
||||
}
|
||||
|
||||
pub struct PollPredictionResultApiResponse {
|
||||
/// The current status of the prediction.
|
||||
pub status: PredictionStatus,
|
||||
|
||||
/// Output URLs. Populated when `status` is `Completed`.
|
||||
pub output_urls: Option<Vec<String>>,
|
||||
|
||||
/// Error message. Populated when `status` is `Failed`.
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
// --- Implementation ---
|
||||
|
||||
pub async fn poll_prediction_result(
|
||||
args: PollPredictionResultArgs<'_>,
|
||||
) -> Result<PollPredictionResultApiResponse, MuapiError> {
|
||||
let url = format!("{}/{}/result", PREDICTIONS_BASE_URL, args.request_id.as_str());
|
||||
|
||||
info!("Polling Muapi prediction result: {}", url);
|
||||
|
||||
let api_key = args.session.api_key.as_str();
|
||||
|
||||
let client = Client::builder()
|
||||
.build()
|
||||
.map_err(|err| MuapiClientError::WreqClientError(err))?;
|
||||
|
||||
let response = client.get(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("x-api-key", api_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| MuapiGenericApiError::WreqError(err))?;
|
||||
|
||||
let status = response.status();
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| MuapiGenericApiError::WreqError(err))?;
|
||||
|
||||
info!("Muapi poll response status: {}, body: {}", status, response_body);
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(MuapiGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: status,
|
||||
body: response_body,
|
||||
}.into());
|
||||
}
|
||||
|
||||
let parsed: PollPredictionResultResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| MuapiGenericApiError::SerdeResponseParseErrorWithBody(err, response_body))?;
|
||||
|
||||
let output_urls = parsed.outputs.and_then(|urls| if urls.is_empty() { None } else { Some(urls) });
|
||||
|
||||
Ok(PollPredictionResultApiResponse {
|
||||
status: PredictionStatus::from_str(&parsed.status),
|
||||
output_urls,
|
||||
error: parsed.error,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key and a valid request_id
|
||||
async fn test_poll_prediction_result() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = get_test_api_key()?;
|
||||
let request_id = RequestId::from_str("5fd7f575-4b9e-4c0a-b7f7-0eb036027b81"); // Corgi shiba I2V
|
||||
let request_id = RequestId::from_str("8a4b578b-4514-443f-b444-4fb3bc5c2cbb"); // Cat TTV
|
||||
let request_id = RequestId::from_str("82d622b1-8911-4cac-8362-e18efaa743ed"); // Corgi I2V
|
||||
let request_id = RequestId::from_str("671c2dee-2ca9-47d7-a12e-d16610b1b365"); // Corgi TTV #1
|
||||
let request_id = RequestId::from_str("8c5303f6-b602-433c-bae8-2d3ef915b7bb"); // Corgi TTV #2
|
||||
|
||||
let result = poll_prediction_result(PollPredictionResultArgs {
|
||||
session: &session,
|
||||
request_id: &request_id,
|
||||
}).await?;
|
||||
println!("Status: {:?}", result.status);
|
||||
println!("Output URLs: {:?}", result.output_urls);
|
||||
println!("Error: {:?}", result.error);
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct PollPredictionResultResponse {
|
||||
pub status: String,
|
||||
pub outputs: Option<Vec<String>>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod seedance_2p0_image_to_video;
|
||||
mod request_types;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
// --- Request ---
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub(super) struct Seedance2p0I2vRequest {
|
||||
pub prompt: String,
|
||||
pub images_list: Vec<String>,
|
||||
pub aspect_ratio: &'static str,
|
||||
pub duration: u8,
|
||||
pub quality: &'static str,
|
||||
}
|
||||
|
||||
// --- Response ---
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct Seedance2p0I2vResponse {
|
||||
pub request_id: String,
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
use crate::api_types::request_id::RequestId;
|
||||
use crate::creds::muapi_session::MuapiSession;
|
||||
use crate::error::muapi_client_error::MuapiClientError;
|
||||
use crate::error::muapi_error::MuapiError;
|
||||
use crate::error::muapi_generic_api_error::MuapiGenericApiError;
|
||||
use crate::requests::seedance_2p0_image_to_video::request_types::*;
|
||||
use log::info;
|
||||
use wreq::Client;
|
||||
|
||||
const SEEDANCE_2P0_I2V_URL: &str = "https://api.muapi.ai/api/v1/seedance-v2.0-i2v";
|
||||
|
||||
// --- Public enums ---
|
||||
|
||||
/// Video aspect ratio.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AspectRatio {
|
||||
/// 16:9 landscape
|
||||
Landscape16x9,
|
||||
/// 9:16 portrait
|
||||
Portrait9x16,
|
||||
/// 4:3 standard
|
||||
Standard4x3,
|
||||
/// 3:4 portrait
|
||||
Portrait3x4,
|
||||
}
|
||||
|
||||
impl AspectRatio {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Landscape16x9 => "16:9",
|
||||
Self::Portrait9x16 => "9:16",
|
||||
Self::Standard4x3 => "4:3",
|
||||
Self::Portrait3x4 => "3:4",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Video duration.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Duration {
|
||||
/// 5 seconds
|
||||
FiveSeconds,
|
||||
/// 10 seconds
|
||||
TenSeconds,
|
||||
/// 15 seconds
|
||||
FifteenSeconds,
|
||||
}
|
||||
|
||||
impl Duration {
|
||||
fn as_u8(&self) -> u8 {
|
||||
match self {
|
||||
Self::FiveSeconds => 5,
|
||||
Self::TenSeconds => 10,
|
||||
Self::FifteenSeconds => 15,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Video quality tier.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Quality {
|
||||
Basic,
|
||||
High,
|
||||
}
|
||||
|
||||
impl Quality {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Basic => "basic",
|
||||
Self::High => "high",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Args & response ---
|
||||
|
||||
pub struct Seedance2p0ImageToVideoArgs<'a> {
|
||||
pub session: &'a MuapiSession,
|
||||
|
||||
/// The prompt describing the desired video.
|
||||
pub prompt: String,
|
||||
|
||||
/// One or more image URLs to use as input frames.
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
/// The aspect ratio for the output video.
|
||||
pub aspect_ratio: AspectRatio,
|
||||
|
||||
/// Duration of the generated video.
|
||||
pub duration: Duration,
|
||||
|
||||
/// Quality tier.
|
||||
pub quality: Quality,
|
||||
}
|
||||
|
||||
impl Seedance2p0ImageToVideoArgs<'_> {
|
||||
/// Estimates the credit cost for this generation request.
|
||||
///
|
||||
/// Pricing (as of 2026-03):
|
||||
///
|
||||
/// | Duration | Basic | High |
|
||||
/// |----------|-------|------|
|
||||
/// | 5s | 0.60 | 1.25 |
|
||||
/// | 10s | 1.20 | 2.50 |
|
||||
/// | 15s | 1.80 | 3.75 |
|
||||
pub fn estimate_credits(&self) -> f64 {
|
||||
match (&self.quality, &self.duration) {
|
||||
(Quality::Basic, Duration::FiveSeconds) => 0.6,
|
||||
(Quality::Basic, Duration::TenSeconds) => 1.2,
|
||||
(Quality::Basic, Duration::FifteenSeconds) => 1.8,
|
||||
(Quality::High, Duration::FiveSeconds) => 1.25,
|
||||
(Quality::High, Duration::TenSeconds) => 2.5,
|
||||
(Quality::High, Duration::FifteenSeconds) => 3.75,
|
||||
}
|
||||
}
|
||||
|
||||
/// Estimates the USD-cent cost for this generation request.
|
||||
///
|
||||
/// Conversion: $1.00 = 1.0 credits.
|
||||
///
|
||||
/// | Duration | Basic | High |
|
||||
/// |----------|-------|------|
|
||||
/// | 5s | 60¢ | 125¢ |
|
||||
/// | 10s | 120¢ | 250¢ |
|
||||
/// | 15s | 180¢ | 375¢ |
|
||||
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
|
||||
let credits = self.estimate_credits();
|
||||
(credits * 100.0).round() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Seedance2p0ImageToVideoArgs<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Seedance2p0ImageToVideoArgs")
|
||||
.field("prompt", &self.prompt)
|
||||
.field("image_urls", &self.image_urls)
|
||||
.field("aspect_ratio", &self.aspect_ratio)
|
||||
.field("duration", &self.duration)
|
||||
.field("quality", &self.quality)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Seedance2p0ImageToVideoResponse {
|
||||
/// The request ID used to poll for results.
|
||||
pub request_id: RequestId,
|
||||
}
|
||||
|
||||
// --- Implementation ---
|
||||
|
||||
pub async fn seedance_2p0_image_to_video(
|
||||
args: Seedance2p0ImageToVideoArgs<'_>,
|
||||
) -> Result<Seedance2p0ImageToVideoResponse, MuapiError> {
|
||||
info!("Submitting Seedance 2.0 i2v task to Muapi: {:?}", args);
|
||||
|
||||
let request_body = Seedance2p0I2vRequest {
|
||||
prompt: args.prompt,
|
||||
images_list: args.image_urls,
|
||||
aspect_ratio: args.aspect_ratio.as_str(),
|
||||
duration: args.duration.as_u8(),
|
||||
quality: args.quality.as_str(),
|
||||
};
|
||||
|
||||
info!("Muapi request body: {:?}", request_body);
|
||||
|
||||
let api_key = args.session.api_key.as_str();
|
||||
|
||||
let client = Client::builder()
|
||||
.build()
|
||||
.map_err(|err| MuapiClientError::WreqClientError(err))?;
|
||||
|
||||
let response = client.post(SEEDANCE_2P0_I2V_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("x-api-key", api_key)
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| MuapiGenericApiError::WreqError(err))?;
|
||||
|
||||
let status = response.status();
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| MuapiGenericApiError::WreqError(err))?;
|
||||
|
||||
info!("Muapi response status: {}, body: {}", status, response_body);
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(MuapiGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: status,
|
||||
body: response_body,
|
||||
}.into());
|
||||
}
|
||||
|
||||
let parsed: Seedance2p0I2vResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| MuapiGenericApiError::SerdeResponseParseErrorWithBody(err, response_body))?;
|
||||
|
||||
Ok(Seedance2p0ImageToVideoResponse {
|
||||
request_id: RequestId::new(parsed.request_id),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
use test_data::web::image_urls::*;
|
||||
|
||||
fn dummy_session() -> MuapiSession {
|
||||
use crate::creds::muapi_api_key::MuapiApiKey;
|
||||
MuapiSession::new(MuapiApiKey::new(String::new()))
|
||||
}
|
||||
|
||||
fn args_with(duration: Duration, quality: Quality) -> Seedance2p0ImageToVideoArgs<'static> {
|
||||
let session = Box::leak(Box::new(dummy_session()));
|
||||
Seedance2p0ImageToVideoArgs {
|
||||
session,
|
||||
prompt: String::new(),
|
||||
image_urls: vec![],
|
||||
aspect_ratio: AspectRatio::Landscape16x9,
|
||||
duration,
|
||||
quality,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_credits() {
|
||||
// Basic
|
||||
assert_eq!(args_with(Duration::FiveSeconds, Quality::Basic).estimate_credits(), 0.6);
|
||||
assert_eq!(args_with(Duration::TenSeconds, Quality::Basic).estimate_credits(), 1.2);
|
||||
assert_eq!(args_with(Duration::FifteenSeconds, Quality::Basic).estimate_credits(), 1.8);
|
||||
|
||||
// High
|
||||
assert_eq!(args_with(Duration::FiveSeconds, Quality::High).estimate_credits(), 1.25);
|
||||
assert_eq!(args_with(Duration::TenSeconds, Quality::High).estimate_credits(), 2.5);
|
||||
assert_eq!(args_with(Duration::FifteenSeconds, Quality::High).estimate_credits(), 3.75);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_in_usd_cents() {
|
||||
// Basic
|
||||
assert_eq!(args_with(Duration::FiveSeconds, Quality::Basic).estimate_cost_in_usd_cents(), 60);
|
||||
assert_eq!(args_with(Duration::TenSeconds, Quality::Basic).estimate_cost_in_usd_cents(), 120);
|
||||
assert_eq!(args_with(Duration::FifteenSeconds, Quality::Basic).estimate_cost_in_usd_cents(), 180);
|
||||
|
||||
// High
|
||||
assert_eq!(args_with(Duration::FiveSeconds, Quality::High).estimate_cost_in_usd_cents(), 125);
|
||||
assert_eq!(args_with(Duration::TenSeconds, Quality::High).estimate_cost_in_usd_cents(), 250);
|
||||
assert_eq!(args_with(Duration::FifteenSeconds, Quality::High).estimate_cost_in_usd_cents(), 375);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key
|
||||
async fn test_seedance_2p0_image_to_video() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = get_test_api_key()?;
|
||||
let args = Seedance2p0ImageToVideoArgs {
|
||||
session: &session,
|
||||
prompt: "The dog barks and runs across the lake's pier, tail wagging.".to_string(),
|
||||
image_urls: vec![
|
||||
JUNO_AT_LAKE_IMAGE_URL.to_string(),
|
||||
],
|
||||
aspect_ratio: AspectRatio::Landscape16x9,
|
||||
duration: Duration::FiveSeconds,
|
||||
quality: Quality::High,
|
||||
};
|
||||
let result = seedance_2p0_image_to_video(args).await?;
|
||||
println!("Result: {:?}", result);
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.as_str().is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key
|
||||
async fn test_seedance_2p0_image_to_video_portrait() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = get_test_api_key()?;
|
||||
let args = Seedance2p0ImageToVideoArgs {
|
||||
session: &session,
|
||||
prompt: "The corgi and shiba swim through the treasure-filled ocean, bubbles rising around them.".to_string(),
|
||||
image_urls: vec![
|
||||
TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(),
|
||||
],
|
||||
aspect_ratio: AspectRatio::Portrait9x16,
|
||||
duration: Duration::FiveSeconds,
|
||||
quality: Quality::High,
|
||||
};
|
||||
let result = seedance_2p0_image_to_video(args).await?;
|
||||
println!("Result: {:?}", result);
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.as_str().is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod seedance_2p0_text_to_video;
|
||||
mod request_types;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
// --- Request ---
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub(super) struct Seedance2p0T2vRequest {
|
||||
pub prompt: String,
|
||||
pub aspect_ratio: &'static str,
|
||||
pub duration: u8,
|
||||
pub quality: &'static str,
|
||||
}
|
||||
|
||||
// --- Response ---
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct Seedance2p0T2vResponse {
|
||||
pub request_id: String,
|
||||
}
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
use crate::api_types::request_id::RequestId;
|
||||
use crate::creds::muapi_session::MuapiSession;
|
||||
use crate::error::muapi_client_error::MuapiClientError;
|
||||
use crate::error::muapi_error::MuapiError;
|
||||
use crate::error::muapi_generic_api_error::MuapiGenericApiError;
|
||||
use crate::requests::seedance_2p0_text_to_video::request_types::*;
|
||||
use log::info;
|
||||
use wreq::Client;
|
||||
|
||||
const SEEDANCE_2P0_T2V_URL: &str = "https://api.muapi.ai/api/v1/seedance-v2.0-t2v";
|
||||
|
||||
// --- Public enums ---
|
||||
|
||||
/// Video aspect ratio.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AspectRatio {
|
||||
/// 16:9 landscape
|
||||
Landscape16x9,
|
||||
/// 9:16 portrait
|
||||
Portrait9x16,
|
||||
/// 4:3 standard
|
||||
Standard4x3,
|
||||
/// 3:4 portrait
|
||||
Portrait3x4,
|
||||
}
|
||||
|
||||
impl AspectRatio {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Landscape16x9 => "16:9",
|
||||
Self::Portrait9x16 => "9:16",
|
||||
Self::Standard4x3 => "4:3",
|
||||
Self::Portrait3x4 => "3:4",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Video duration.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Duration {
|
||||
/// 5 seconds
|
||||
FiveSeconds,
|
||||
/// 10 seconds
|
||||
TenSeconds,
|
||||
/// 15 seconds
|
||||
FifteenSeconds,
|
||||
}
|
||||
|
||||
impl Duration {
|
||||
fn as_u8(&self) -> u8 {
|
||||
match self {
|
||||
Self::FiveSeconds => 5,
|
||||
Self::TenSeconds => 10,
|
||||
Self::FifteenSeconds => 15,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Video quality tier.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Quality {
|
||||
Basic,
|
||||
High,
|
||||
}
|
||||
|
||||
impl Quality {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Basic => "basic",
|
||||
Self::High => "high",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Args & response ---
|
||||
|
||||
pub struct Seedance2p0TextToVideoArgs<'a> {
|
||||
pub session: &'a MuapiSession,
|
||||
|
||||
/// The prompt describing the desired video.
|
||||
pub prompt: String,
|
||||
|
||||
/// The aspect ratio for the output video.
|
||||
pub aspect_ratio: AspectRatio,
|
||||
|
||||
/// Duration of the generated video.
|
||||
pub duration: Duration,
|
||||
|
||||
/// Quality tier.
|
||||
pub quality: Quality,
|
||||
}
|
||||
|
||||
impl Seedance2p0TextToVideoArgs<'_> {
|
||||
/// Estimates the credit cost for this generation request.
|
||||
///
|
||||
/// Pricing (as of 2026-03):
|
||||
///
|
||||
/// | Duration | Basic | High |
|
||||
/// |----------|-------|------|
|
||||
/// | 5s | 0.60 | 1.25 |
|
||||
/// | 10s | 1.20 | 2.50 |
|
||||
/// | 15s | 1.80 | 3.75 |
|
||||
pub fn estimate_credits(&self) -> f64 {
|
||||
match (&self.quality, &self.duration) {
|
||||
(Quality::Basic, Duration::FiveSeconds) => 0.6,
|
||||
(Quality::Basic, Duration::TenSeconds) => 1.2,
|
||||
(Quality::Basic, Duration::FifteenSeconds) => 1.8,
|
||||
(Quality::High, Duration::FiveSeconds) => 1.25,
|
||||
(Quality::High, Duration::TenSeconds) => 2.5,
|
||||
(Quality::High, Duration::FifteenSeconds) => 3.75,
|
||||
}
|
||||
}
|
||||
|
||||
/// Estimates the USD-cent cost for this generation request.
|
||||
///
|
||||
/// Conversion: $1.00 = 1.0 credits.
|
||||
///
|
||||
/// | Duration | Basic | High |
|
||||
/// |----------|-------|------|
|
||||
/// | 5s | 60¢ | 125¢ |
|
||||
/// | 10s | 120¢ | 250¢ |
|
||||
/// | 15s | 180¢ | 375¢ |
|
||||
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
|
||||
let credits = self.estimate_credits();
|
||||
(credits * 100.0).round() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Seedance2p0TextToVideoArgs<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Seedance2p0TextToVideoArgs")
|
||||
.field("prompt", &self.prompt)
|
||||
.field("aspect_ratio", &self.aspect_ratio)
|
||||
.field("duration", &self.duration)
|
||||
.field("quality", &self.quality)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Seedance2p0TextToVideoResponse {
|
||||
/// The request ID used to poll for results.
|
||||
pub request_id: RequestId,
|
||||
}
|
||||
|
||||
// --- Implementation ---
|
||||
|
||||
pub async fn seedance_2p0_text_to_video(
|
||||
args: Seedance2p0TextToVideoArgs<'_>,
|
||||
) -> Result<Seedance2p0TextToVideoResponse, MuapiError> {
|
||||
info!("Submitting Seedance 2.0 t2v task to Muapi: {:?}", args);
|
||||
|
||||
let request_body = Seedance2p0T2vRequest {
|
||||
prompt: args.prompt,
|
||||
aspect_ratio: args.aspect_ratio.as_str(),
|
||||
duration: args.duration.as_u8(),
|
||||
quality: args.quality.as_str(),
|
||||
};
|
||||
|
||||
info!("Muapi request body: {:?}", request_body);
|
||||
|
||||
let api_key = args.session.api_key.as_str();
|
||||
|
||||
let client = Client::builder()
|
||||
.build()
|
||||
.map_err(|err| MuapiClientError::WreqClientError(err))?;
|
||||
|
||||
let response = client.post(SEEDANCE_2P0_T2V_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("x-api-key", api_key)
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| MuapiGenericApiError::WreqError(err))?;
|
||||
|
||||
let status = response.status();
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| MuapiGenericApiError::WreqError(err))?;
|
||||
|
||||
info!("Muapi response status: {}, body: {}", status, response_body);
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(MuapiGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: status,
|
||||
body: response_body,
|
||||
}.into());
|
||||
}
|
||||
|
||||
let parsed: Seedance2p0T2vResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| MuapiGenericApiError::SerdeResponseParseErrorWithBody(err, response_body))?;
|
||||
|
||||
Ok(Seedance2p0TextToVideoResponse {
|
||||
request_id: RequestId::new(parsed.request_id),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::creds::muapi_api_key::MuapiApiKey;
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
|
||||
fn dummy_session() -> MuapiSession {
|
||||
MuapiSession::new(MuapiApiKey::new(String::new()))
|
||||
}
|
||||
|
||||
fn args_with(duration: Duration, quality: Quality) -> Seedance2p0TextToVideoArgs<'static> {
|
||||
let session = Box::leak(Box::new(dummy_session()));
|
||||
Seedance2p0TextToVideoArgs {
|
||||
session,
|
||||
prompt: String::new(),
|
||||
aspect_ratio: AspectRatio::Landscape16x9,
|
||||
duration,
|
||||
quality,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_credits() {
|
||||
// Basic
|
||||
assert_eq!(args_with(Duration::FiveSeconds, Quality::Basic).estimate_credits(), 0.6);
|
||||
assert_eq!(args_with(Duration::TenSeconds, Quality::Basic).estimate_credits(), 1.2);
|
||||
assert_eq!(args_with(Duration::FifteenSeconds, Quality::Basic).estimate_credits(), 1.8);
|
||||
|
||||
// High
|
||||
assert_eq!(args_with(Duration::FiveSeconds, Quality::High).estimate_credits(), 1.25);
|
||||
assert_eq!(args_with(Duration::TenSeconds, Quality::High).estimate_credits(), 2.5);
|
||||
assert_eq!(args_with(Duration::FifteenSeconds, Quality::High).estimate_credits(), 3.75);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_in_usd_cents() {
|
||||
// Basic
|
||||
assert_eq!(args_with(Duration::FiveSeconds, Quality::Basic).estimate_cost_in_usd_cents(), 60);
|
||||
assert_eq!(args_with(Duration::TenSeconds, Quality::Basic).estimate_cost_in_usd_cents(), 120);
|
||||
assert_eq!(args_with(Duration::FifteenSeconds, Quality::Basic).estimate_cost_in_usd_cents(), 180);
|
||||
|
||||
// High
|
||||
assert_eq!(args_with(Duration::FiveSeconds, Quality::High).estimate_cost_in_usd_cents(), 125);
|
||||
assert_eq!(args_with(Duration::TenSeconds, Quality::High).estimate_cost_in_usd_cents(), 250);
|
||||
assert_eq!(args_with(Duration::FifteenSeconds, Quality::High).estimate_cost_in_usd_cents(), 375);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key
|
||||
async fn test_seedance_2p0_text_to_video_landscape() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = get_test_api_key()?;
|
||||
let args = Seedance2p0TextToVideoArgs {
|
||||
session: &session,
|
||||
prompt: "A corgi walks past a lighthouse on the shore. In the background, mossy rocks and rolling green hills dot the landscape. The sand is black, and the air is full of fog and mist".to_string(),
|
||||
aspect_ratio: AspectRatio::Landscape16x9,
|
||||
duration: Duration::FiveSeconds,
|
||||
quality: Quality::High,
|
||||
};
|
||||
let result = seedance_2p0_text_to_video(args).await?;
|
||||
println!("Result: {:?}", result);
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.as_str().is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key
|
||||
async fn test_seedance_2p0_text_to_video_portrait() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = get_test_api_key()?;
|
||||
let args = Seedance2p0TextToVideoArgs {
|
||||
session: &session,
|
||||
prompt: "A cat sits on a windowsill watching rain fall outside. Lightning flashes and the cat's eyes widen.".to_string(),
|
||||
aspect_ratio: AspectRatio::Portrait9x16,
|
||||
duration: Duration::TenSeconds,
|
||||
quality: Quality::Basic,
|
||||
};
|
||||
let result = seedance_2p0_text_to_video(args).await?;
|
||||
println!("Result: {:?}", result);
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.as_str().is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::creds::muapi_session::MuapiSession;
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn get_test_api_key() -> AnyhowResult<MuapiSession> {
|
||||
let api_key = read_to_string("/Users/bt/Artcraft/credentials/muapi_api_key.txt")?;
|
||||
let api_key = api_key.trim().to_string();
|
||||
Ok(MuapiSession::from_api_key_string(api_key))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user