Compare commits

..
Author SHA1 Message Date
Brandon Thomas a569947608 cleanup 2026-03-05 21:20:22 -05:00
Brandon Thomas 9d66a880f9 codegen 2026-03-05 21:02:56 -05:00
Brandon Thomas 7c2b0f310e Handler to record tracking data 2026-03-05 21:01:51 -05:00
572 changed files with 1729 additions and 31286 deletions
-2
View File
@@ -6,7 +6,6 @@
.env-elasticsearch-secrets
.env-migration-tool-secrets
.env-secrets
.env-support-tool-secrets
_develop/tools/.secrets
_develop/tools/db_secrets.toml
crates/service/web/storyteller_web/config/storyteller-web.development-secrets.env
@@ -37,7 +36,6 @@ script/socat_dev_database_connect.sh
venv/
# Testing
/prompts/
/requests/
# Scratch data
@@ -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 status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 14
},
"nullable": []
},
"hash": "002cdb3d086f3b4181e2997befa54bd8516cb1d3d3bb6e389862695318d5c299"
}
@@ -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 frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 16
},
"nullable": []
},
"hash": "29916e73a1784ea0b70ef4f637520b05efccc31e01840f681e9b042f82eb8f7e"
}
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n on_complete_primary_media_file_token,\n on_complete_primary_media_file_class,\n on_complete_batch_token,\n on_complete_primary_media_file_cdn_url,\n on_complete_primary_media_file_thumbnail_url_template,\n on_failure_type,\n on_failure_message,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n on_complete_primary_media_file_token,\n on_complete_primary_media_file_class,\n on_complete_batch_token,\n on_complete_primary_media_file_cdn_url,\n on_complete_primary_media_file_thumbnail_url_template,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
"describe": {
"columns": [
{
@@ -73,29 +73,19 @@
"ordinal": 13,
"type_info": "Text"
},
{
"name": "on_failure_type",
"ordinal": 14,
"type_info": "Text"
},
{
"name": "on_failure_message",
"ordinal": 15,
"type_info": "Text"
},
{
"name": "created_at: DateTime<Utc>",
"ordinal": 16,
"ordinal": 14,
"type_info": "Int64"
},
{
"name": "updated_at: DateTime<Utc>",
"ordinal": 17,
"ordinal": 15,
"type_info": "Int64"
},
{
"name": "completed_at: DateTime<Utc>",
"ordinal": 18,
"ordinal": 16,
"type_info": "Int64"
}
],
@@ -117,12 +107,10 @@
true,
true,
true,
true,
true,
false,
false,
true
]
},
"hash": "2436eb7bf4b0e23a899311a67a77f71d89eb35461e2113e43cd9d221bfa91c9a"
"hash": "47f94c127abc20683b25c257aed3c2190b9bd29eb829d61392f08e10fd986767"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO wallets\nSET\n token = ?,\n wallet_namespace = ?,\n\n owner_user_token = ?,\n\n banked_credits = 0,\n monthly_credits = 0\n ",
"query": "\nINSERT INTO wallets\nSET\n token = ?,\n wallet_namespace = ?,\n\n owner_user_token = ?,\n \n banked_credits = 0,\n monthly_credits = 0\n ",
"describe": {
"columns": [],
"parameters": {
@@ -8,5 +8,5 @@
},
"nullable": []
},
"hash": "f4fb945b4311bd4458a68d82e417b40f8c36c08d572c07881b89cd157a8a8699"
"hash": "4f5100a2cfb465964e77eaacb1ae394daae94de20c56a0966a8222dc9bea63c6"
}
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_cover_image_media_file_token = ?,\n\n maybe_mod_user_token = ?,\n is_generated_on_prem = ?,\n generated_by_worker = ?,\n generated_by_cluster = ?\n ",
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_mod_user_token = ?, \n is_generated_on_prem = ?, \n generated_by_worker = ?, \n generated_by_cluster = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 40
"Right": 39
},
"nullable": []
},
"hash": "87d5511700985daaabfc9f2972f018d56b6672c07be082daa7c1a782400cb18d"
"hash": "b4470e607c005421dd8504b9f1d695080a581936ac6ce00b6f437b2764e741be"
}
@@ -1,58 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n username,\n display_name,\n username_is_generated as `username_is_generated: bool`,\n username_is_not_customized as `username_is_not_customized: bool`\nFROM users\nWHERE\n token = ?\n AND user_deleted_at IS NULL\n AND mod_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 1,
"name": "display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "username_is_generated: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 3,
"name": "username_is_not_customized: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "be9867966f24f44e14f96966cf5113055f00c339dda75334e7e88d355b9bfe72"
}
@@ -1,102 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `operation_id`,\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 jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "operation_id",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 256
}
},
{
"ordinal": 2,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "creator_ip_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 5,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | ENUM",
"char_set": 224,
"max_size": 28
}
},
{
"ordinal": 6,
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
true,
true,
false,
false,
true,
true
]
},
"hash": "d56a26a7300495fac9741e77e390f16402e2273dbab9a15d7c17eb1f0da7a1b1"
}
Generated
+20 -88
View File
@@ -578,9 +578,9 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.13"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
[[package]]
name = "anstyle-parse"
@@ -797,7 +797,6 @@ dependencies = [
"tokio",
"uuid 1.18.1",
"workspace-build-acceleration",
"world_labs_api",
]
[[package]]
@@ -2225,9 +2224,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.60"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
dependencies = [
"clap_builder",
"clap_derive",
@@ -2235,21 +2234,21 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.60"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
dependencies = [
"anstream",
"anstyle",
"clap_lex 1.0.0",
"clap_lex 0.7.4",
"strsim 0.11.1",
]
[[package]]
name = "clap_derive"
version = "4.5.55"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -2268,9 +2267,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "1.0.0"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "cloud_storage"
@@ -3068,7 +3067,7 @@ version = "0.0.1"
dependencies = [
"anyhow",
"chrono",
"clap 4.5.60",
"clap 4.5.13",
"datetimes",
"dotenv",
"easyenv",
@@ -3103,7 +3102,7 @@ name = "db-cleanup"
version = "0.0.1"
dependencies = [
"bucket_paths",
"clap 4.5.60",
"clap 4.5.13",
"dotenv",
"easyenv",
"enums",
@@ -3263,7 +3262,7 @@ name = "dev-database-seed"
version = "0.0.1"
dependencies = [
"bucket_paths",
"clap 4.5.60",
"clap 4.5.13",
"cloud_storage",
"config",
"easyenv",
@@ -3289,7 +3288,7 @@ name = "dev-upload-media-file"
version = "0.0.1"
dependencies = [
"bucket_paths",
"clap 4.5.60",
"clap 4.5.13",
"cloud_storage",
"config",
"easyenv",
@@ -3763,7 +3762,7 @@ name = "elasticsearch-cli"
version = "0.0.1"
dependencies = [
"bucket_paths",
"clap 4.5.60",
"clap 4.5.13",
"config",
"easyenv",
"elasticsearch",
@@ -4186,7 +4185,6 @@ dependencies = [
"log",
"reqwest 0.12.23",
"serde",
"strum 0.28.0",
"test_data",
"testing",
"tokio",
@@ -6744,7 +6742,7 @@ dependencies = [
name = "lru-delete-models"
version = "0.0.1"
dependencies = [
"clap 4.5.60",
"clap 4.5.13",
"errors",
"logging",
"walkdir",
@@ -7054,7 +7052,7 @@ name = "migration-tool"
version = "0.0.1"
dependencies = [
"bucket_paths",
"clap 4.5.60",
"clap 4.5.13",
"cloud_storage",
"config",
"easyenv",
@@ -10316,7 +10314,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
name = "scrape-test"
version = "0.0.1"
dependencies = [
"clap 4.5.60",
"clap 4.5.13",
"errors",
"tokio",
"web_scrapers",
@@ -11604,7 +11602,6 @@ dependencies = [
"utoipa-swagger-ui",
"videos",
"workspace-build-acceleration",
"world_labs_api",
"wreq",
"zip 0.6.6",
]
@@ -11749,20 +11746,6 @@ dependencies = [
"rsa",
]
[[package]]
name = "support-tool"
version = "0.0.1"
dependencies = [
"anyhow",
"clap 4.5.60",
"easyenv",
"errors",
"log",
"seedance2pro",
"serde_json",
"tokio",
]
[[package]]
name = "swift-rs"
version = "1.0.7"
@@ -14928,7 +14911,7 @@ dependencies = [
"bytes",
"cc",
"chrono",
"clap 4.5.60",
"clap 4.5.13",
"cookie 0.18.1",
"core-foundation-sys",
"crossbeam-utils",
@@ -15023,26 +15006,6 @@ dependencies = [
"zstd-sys",
]
[[package]]
name = "world_labs_api"
version = "0.0.1"
dependencies = [
"anyhow",
"chrono",
"env",
"env_logger 0.11.9",
"errors",
"filesys",
"log",
"serde",
"serde_json",
"strum 0.28.0",
"tokio",
"uuid 1.18.1",
"workspace-build-acceleration",
"wreq",
]
[[package]]
name = "world_labs_client"
version = "0.0.1"
@@ -15081,37 +15044,6 @@ dependencies = [
"wreq-util",
]
[[package]]
name = "worldlabs-job"
version = "0.0.1"
dependencies = [
"actix-web",
"actix_helpers",
"anyhow",
"bootstrap",
"bucket_paths",
"cloud_storage",
"concurrency",
"config",
"easyenv",
"enums",
"errors",
"hashing",
"jobs_common",
"log",
"mysql_queries",
"reqwest 0.12.23",
"serde",
"serde_derive",
"serde_json",
"server_environment",
"sqlx",
"tokens",
"tokio",
"workspace-build-acceleration",
"world_labs_api",
]
[[package]]
name = "wreq"
version = "6.0.0-rc.28"
-6
View File
@@ -19,7 +19,6 @@ members = [
"crates/api_clients/seedance2pro",
"crates/api_clients/runwayml_client",
"crates/api_clients/thumbnail_generator",
"crates/api_clients/world_labs_api",
"crates/api_clients/world_labs_client",
# CLI tools for operations, etc.
@@ -33,7 +32,6 @@ members = [
"crates/cli/production/kubectl-pod-cleanup",
"crates/cli/production/lru-delete-models",
"crates/cli/production/migration-tool",
"crates/cli/production/support-tool",
# Desktop apps
"crates/desktop/artcraft",
@@ -121,7 +119,6 @@ members = [
"crates/service/job/es-update-job",
"crates/service/job/inference_job",
"crates/service/job/seedance2_pro_job",
"crates/service/job/worldlabs_job",
"crates/service/job/tts_download_job",
"crates/service/job/tts_inference_job",
"crates/service/job/w2l_download_job",
@@ -223,7 +220,6 @@ component_traits = { path = "crates/service/plugins/component_traits" }
cookie_store = { path = "crates/lib/cookie_store" }
cookies = { path = "crates/lib/cookies" }
crockford = { path = "crates/lib/data/crockford" }
easyenv = { path = "crates/lib/easyenv" }
enums = { path = "crates/schema/public/enums" }
enums_public = { path = "crates/schema/public/enums_public" }
errors = { path = "crates/lib/errors" }
@@ -252,7 +248,6 @@ tokens = { path = "crates/schema/public/tokens" }
url_utils = { path = "crates/lib/url_utils" }
users = { path = "crates/lib/users" }
web_base64 = { path = "crates/lib/data/web_base64" }
world_labs_api = { path = "crates/api_clients/world_labs_api" }
world_labs_client = { path = "crates/api_clients/world_labs_client" }
# External libs
@@ -273,7 +268,6 @@ base64 = "0.22.1"
bstr = "1.12.1"
bytes = "1.10.1"
chrono = { version = "0.4.41", features = ["serde"] }
clap = { version = "4.5.60", features = ["derive"] }
cookie = "0.18.1"
env = "1.0.1"
env_logger = "0.11.9"
+1 -1
View File
@@ -55,7 +55,7 @@ We haven't abandoned text-to-asset generation for quick prototyping and ideation
| Provider | Features |
|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Artcraft | Nano Banana, Nano Banana Pro, GPT-Image-1 / 1.5, Seedance 1.0 Lite / 1.5 Pro / 2.0, Seedream 4 / 4.5, Flux 1.1 / Kontext, Veo 2 / 3 / 3.1, Kling 1.6 / 2.1 / 2.5 / 2.6, Sora 2 / Pro, Hunyuan 3d 2 / 3 |
| Artcraft | Nano Banana, Nano Banana Pro, GPT-Image-1 / 1.5, Seedance 2 / Seedream 4 / 4.5, Flux 1.1 / Kontext, Veo 2 / 3 / 3.1, Kling 1.6 / 2.1 / 2.5 / 2.6, Seedance, Sora 2 / Pro, Hunyuan 3d 2 / 3 |
| Grok | Grok Imagine, Grok Video |
| Midjourney | Image Gen (all versions) |
| Sora | Sora 1, Sora 2, GPT-Image-1 |
@@ -16,9 +16,7 @@
-- on_complete_primary_media_file_class,
-- on_complete_primary_media_file_cdn_url,
-- on_complete_primary_media_file_thumbnail_url_template,
-- tasks_v6.sqlite - added two fields:
-- on_failure_type
-- on_failure_message
--
CREATE TABLE tasks (
-- Task auto-incrementing primary key.
@@ -86,15 +84,6 @@ CREATE TABLE tasks (
-- The URL template for the thumbnail of the primary media file.
on_complete_primary_media_file_thumbnail_url_template TEXT,
-- OPTIONAL.
-- An enum-like string describing the failure reason (even for non-terminal failures).
-- The enum is `TaskFailureType`, which is a close analogue of the backend API's `FrontendFailureCategory`.
on_failure_type TEXT,
-- OPTIONAL.
-- A human-readable message describing the failure reason (even for non-terminal failures).
on_failure_message TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
completed_at INTEGER DEFAULT NULL
@@ -1,8 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE web_referrals
MODIFY COLUMN utm_source VARCHAR(32) DEFAULT NULL,
MODIFY COLUMN utm_medium VARCHAR(32) DEFAULT NULL,
MODIFY COLUMN utm_campaign VARCHAR(255) DEFAULT NULL;
@@ -1,8 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE web_referrals
MODIFY COLUMN utm_source VARCHAR(150) DEFAULT NULL,
MODIFY COLUMN utm_medium VARCHAR(150) DEFAULT NULL,
MODIFY COLUMN utm_campaign VARCHAR(150) DEFAULT NULL;
@@ -13,11 +13,11 @@ CREATE TABLE web_referrals (
-- The parsed out referral domain
domain VARCHAR(255) DEFAULT NULL,
utm_source VARCHAR(150) DEFAULT NULL,
utm_source VARCHAR(32) DEFAULT NULL,
utm_medium VARCHAR(150) DEFAULT NULL,
utm_medium VARCHAR(32) DEFAULT NULL,
utm_campaign VARCHAR(150) DEFAULT NULL,
utm_campaign VARCHAR(255) DEFAULT NULL,
ip_address VARCHAR(40) NOT NULL,
-7
View File
@@ -143,12 +143,6 @@ RUN SQLX_OFFLINE=true \
--release \
--bin seedance2-pro-job
RUN SQLX_OFFLINE=true \
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
$HOME/.cargo/bin/cargo build \
--release \
--bin worldlabs-job
RUN SQLX_OFFLINE=true \
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
$HOME/.cargo/bin/cargo build \
@@ -209,7 +203,6 @@ RUN echo -n ${GIT_SHA} > GIT_SHA
# Copy all the binaries (except those that need a GPU):
COPY --from=builder /tmp/target/release/storyteller-web /
COPY --from=builder /tmp/target/release/seedance2-pro-job /
COPY --from=builder /tmp/target/release/worldlabs-job /
COPY --from=builder /tmp/target/release/dummy-service /
COPY --from=builder /tmp/target/release/analytics-job /
COPY --from=builder /tmp/target/release/email-sender-job /
@@ -1,59 +0,0 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use enums::common::generation::common_splat_model::CommonSplatModel;
use enums::common::generation_provider::GenerationProvider;
pub const ESTIMATE_SPLAT_COST_PATH: &str = "/v1/generate/cost_estimate/splat";
/// Request body for the splat cost estimate endpoint.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EstimateSplatCostRequest {
/// The splat model to estimate costs for.
pub model: CommonSplatModel,
/// The provider to route the generation through.
pub provider: GenerationProvider,
/// Whether the request includes a reference image.
pub has_reference_image: Option<bool>,
}
/// Response body for the splat cost estimate endpoint.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EstimateSplatCostResponse {
pub success: bool,
/// Estimated cost in credits.
pub cost_in_credits: Option<u64>,
/// Estimated cost in USD cents.
pub cost_in_usd_cents: Option<u64>,
/// Whether the generation is free for this user/plan.
pub is_free: bool,
/// Whether the user has unlimited generations.
pub is_unlimited: bool,
/// Whether the user is rate limited.
pub is_rate_limited: bool,
/// Whether the output will have a watermark.
pub has_watermark: bool,
}
/// Error response for the splat cost estimate endpoint.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EstimateSplatCostError {
pub success: bool,
pub error_type: EstimateSplatCostErrorType,
pub error_message: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum EstimateSplatCostErrorType {
InvalidProviderForModel,
InvalidInput,
}
@@ -9,10 +9,7 @@ use enums::common::generation_provider::GenerationProvider;
pub const ESTIMATE_VIDEO_COST_PATH: &str = "/v1/generate/cost_estimate/video";
/// Request body for the video cost estimate endpoint.
///
/// Every model cares about its own set of parameters and not all
/// parameters may be relevant for every model.
#[derive(Serialize, Deserialize, ToSchema, Debug)]
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EstimateVideoCostRequest {
/// The video model to estimate costs for.
pub model: CommonVideoModel,
@@ -37,13 +34,10 @@ pub struct EstimateVideoCostRequest {
/// Number of videos to generate in parallel.
pub video_batch_count: Option<u16>,
/// Whether to generate audio for the video.
pub generate_audio: Option<bool>,
}
/// Describes the type of video generation being requested.
#[derive(Serialize, Deserialize, ToSchema, Debug)]
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum GenerationMode {
TextToVideo,
@@ -105,7 +99,6 @@ mod tests {
resolution: None,
duration_seconds: None,
video_batch_count: None,
generate_audio: None,
};
let serialized = serde_json::to_string(&request).unwrap();
assert!(serialized.contains("\"generation_mode\":{\"type\":\"text_to_video\"}"));
@@ -122,7 +115,6 @@ mod tests {
resolution: None,
duration_seconds: None,
video_batch_count: None,
generate_audio: None,
};
let serialized = serde_json::to_string(&request).unwrap();
assert!(serialized.contains("\"generation_mode\":{\"type\":\"reference_image_to_video\",\"count\":1}"));
@@ -1,3 +1,2 @@
pub mod estimate_image_cost;
pub mod estimate_splat_cost;
pub mod estimate_video_cost;
@@ -1,56 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const FLUX_2_LORA_EDIT_IMAGE_ANGLE_PATH: &str = "/v1/generate/image/angle/flux_2_lora";
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct Flux2LoraEditImageAngleRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// The image to edit with angle adjustment.
pub image_media_token: MediaFileToken,
/// Horizontal rotation angle in degrees.
pub horizontal_angle: Option<f64>,
/// Vertical rotation angle in degrees.
pub vertical_angle: Option<f64>,
/// Zoom level.
pub zoom: Option<f64>,
/// Number of images to generate. Default is one.
pub num_images: Option<Flux2LoraEditImageAngleNumImages>,
/// Output image size. Default is square_hd.
pub image_size: Option<Flux2LoraEditImageAngleImageSize>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Flux2LoraEditImageAngleNumImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Flux2LoraEditImageAngleImageSize {
Square,
SquareHd,
PortraitFourThree,
PortraitSixteenNine,
LandscapeFourThree,
LandscapeSixteenNine,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Flux2LoraEditImageAngleResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1,2 +0,0 @@
pub mod flux_2_lora_edit_image_angle;
pub mod qwen_edit_2511_edit_image_angle;
@@ -1,59 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const QWEN_EDIT_2511_EDIT_IMAGE_ANGLE_PATH: &str = "/v1/generate/image/angle/qwen_edit_2511";
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct QwenEdit2511EditImageAngleRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// The image to edit with angle adjustment.
pub image_media_token: MediaFileToken,
/// Horizontal rotation angle in degrees.
pub horizontal_angle: Option<f64>,
/// Vertical rotation angle in degrees.
pub vertical_angle: Option<f64>,
/// Zoom level.
pub zoom: Option<f64>,
/// Additional text prompt to guide the generation.
pub additional_prompt: Option<String>,
/// Number of images to generate. Default is one.
pub num_images: Option<QwenEdit2511EditImageAngleNumImages>,
/// Output image size. Default is square_hd.
pub image_size: Option<QwenEdit2511EditImageAngleImageSize>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum QwenEdit2511EditImageAngleNumImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum QwenEdit2511EditImageAngleImageSize {
Square,
SquareHd,
PortraitFourThree,
PortraitSixteenNine,
LandscapeFourThree,
LandscapeSixteenNine,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct QwenEdit2511EditImageAngleResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1,4 +1,3 @@
pub mod angle;
pub mod bg_removal;
pub mod edit;
pub mod inpaint;
@@ -1,5 +1,4 @@
pub mod cost_estimate;
pub mod image;
pub mod object;
pub mod splat;
pub mod video;
@@ -1,25 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GENERATE_WORLDLABS_MARBLE_0P1_MINI_SPLAT_URL_PATH: &str = "/v1/generate/splat/worldlabs_marble_0p1_mini";
/// World Labs Marble 0.1-mini Splat Generation
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateWorldlabsMarble0p1MiniSplatRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Optional image to use as input for world generation.
pub image_media_file_token: Option<MediaFileToken>,
/// Optional text prompt for world generation.
pub prompt: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateWorldlabsMarble0p1MiniSplatResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1,25 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GENERATE_WORLDLABS_MARBLE_0P1_PLUS_SPLAT_URL_PATH: &str = "/v1/generate/splat/worldlabs_marble_0p1_plus";
/// World Labs Marble 0.1-plus Splat Generation
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateWorldlabsMarble0p1PlusSplatRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Optional image to use as input for world generation.
pub image_media_file_token: Option<MediaFileToken>,
/// Optional text prompt for world generation.
pub prompt: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateWorldlabsMarble0p1PlusSplatResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1,2 +0,0 @@
pub mod generate_worldlabs_marble_0p1_mini_splat;
pub mod generate_worldlabs_marble_0p1_plus_splat;
@@ -1,66 +0,0 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const KLING_3P0_PRO_MULTI_FUNCTION_VIDEO_PATH: &str = "/v1/generate/video/multi_function/kling_3p0_pro";
/// Both text-to-video and image-to-video in one request.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Kling3p0ProMultiFunctionVideoGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt.
pub prompt: Option<String>,
/// Optional negative prompt.
pub negative_prompt: Option<String>,
/// If present, we're doing image-to-video. If absent, text-to-video.
pub image_media_token: Option<MediaFileToken>,
/// Optional end frame image.
pub end_image_media_token: Option<MediaFileToken>,
/// Duration of the video (3-15 seconds).
pub duration: Option<Kling3p0ProMultiFunctionVideoGenDuration>,
/// Whether to generate audio.
pub generate_audio: Option<bool>,
/// Aspect ratio (only for text-to-video).
pub aspect_ratio: Option<Kling3p0ProMultiFunctionVideoGenAspectRatio>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Kling3p0ProMultiFunctionVideoGenDuration {
ThreeSeconds,
FourSeconds,
FiveSeconds,
SixSeconds,
SevenSeconds,
EightSeconds,
NineSeconds,
TenSeconds,
ElevenSeconds,
TwelveSeconds,
ThirteenSeconds,
FourteenSeconds,
FifteenSeconds,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Kling3p0ProMultiFunctionVideoGenAspectRatio {
Square,
SixteenByNine,
NineBySixteen,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Kling3p0ProMultiFunctionVideoGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1,66 +0,0 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const KLING_3P0_STANDARD_MULTI_FUNCTION_VIDEO_PATH: &str = "/v1/generate/video/multi_function/kling_3p0_standard";
/// Both text-to-video and image-to-video in one request.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Kling3p0StandardMultiFunctionVideoGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt.
pub prompt: Option<String>,
/// Optional negative prompt.
pub negative_prompt: Option<String>,
/// If present, we're doing image-to-video. If absent, text-to-video.
pub image_media_token: Option<MediaFileToken>,
/// Optional end frame image.
pub end_image_media_token: Option<MediaFileToken>,
/// Duration of the video (3-15 seconds).
pub duration: Option<Kling3p0StandardMultiFunctionVideoGenDuration>,
/// Whether to generate audio.
pub generate_audio: Option<bool>,
/// Aspect ratio (only for text-to-video).
pub aspect_ratio: Option<Kling3p0StandardMultiFunctionVideoGenAspectRatio>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Kling3p0StandardMultiFunctionVideoGenDuration {
ThreeSeconds,
FourSeconds,
FiveSeconds,
SixSeconds,
SevenSeconds,
EightSeconds,
NineSeconds,
TenSeconds,
ElevenSeconds,
TwelveSeconds,
ThirteenSeconds,
FourteenSeconds,
FifteenSeconds,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Kling3p0StandardMultiFunctionVideoGenAspectRatio {
Square,
SixteenByNine,
NineBySixteen,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Kling3p0StandardMultiFunctionVideoGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1,8 +1,5 @@
pub mod kling_2_5_turbo_multi_function_video_gen;
pub mod kling_2_6_multi_function_video_gen;
pub mod kling_3p0_pro_multi_function_video_gen;
pub mod kling_3p0_standard_multi_function_video_gen;
pub mod seedance_1p5_pro_multi_function_video_gen;
pub mod seedance_2p0_multi_function_video_gen;
pub mod sora_2_multi_function_video_gen;
pub mod sora_2_pro_multi_function_video_gen;
@@ -1,85 +0,0 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const SEEDANCE_1P5_PRO_MULTI_FUNCTION_VIDEO_GEN_PATH: &str = "/v1/generate/video/multi_function/seedance_1p5_pro";
/// Both text-to-video and image-to-video in one request.
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct Seedance1p5ProMultiFunctionVideoGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// "Required".
/// Required downstream, but we'll coerce null to empty string.
/// Text prompt.
pub prompt: Option<String>,
/// Optional.
/// Only for image-to-video.
/// Source of the starting frame.
/// If present, we're doing image-to-video.
/// If absent, we're doing text-to-video.
pub start_frame_image_media_token: Option<MediaFileToken>,
/// Optional.
/// Only for image-to-video.
/// Source of the ending frame.
pub end_frame_image_media_token: Option<MediaFileToken>,
/// Optional.
pub resolution: Option<Seedance1p5ProMultiFunctionVideoGenResolution>,
/// Optional.
/// Duration of the video.
/// (this is uniform against all modes)
pub duration: Option<Seedance1p5ProMultiFunctionVideoGenDuration>,
/// Optional.
pub aspect_ratio: Option<Seedance1p5ProMultiFunctionVideoGenAspectRatio>,
/// Whether to generate audio.
pub generate_audio: Option<bool>,
}
#[derive(Serialize, Deserialize, ToSchema, Copy, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum Seedance1p5ProMultiFunctionVideoGenResolution {
FourEightyP,
SevenTwentyP,
TenEightyP,
}
#[derive(Serialize, Deserialize, ToSchema, Copy, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum Seedance1p5ProMultiFunctionVideoGenDuration {
FourSeconds,
FiveSeconds,
SixSeconds,
SevenSeconds,
EightSeconds,
NineSeconds,
TenSeconds,
ElevenSeconds,
TwelveSeconds,
}
#[derive(Serialize, Deserialize, ToSchema, Copy, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum Seedance1p5ProMultiFunctionVideoGenAspectRatio {
TwentyOneByNine,
SixteenByNine,
FourByThree,
Square,
ThreeByFour,
NineBySixteen,
Auto,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Seedance1p5ProMultiFunctionVideoGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -9,8 +9,6 @@ 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";
@@ -100,16 +98,9 @@ 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.
#[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>,
pub maybe_failure_category: Option<FrontendFailureCategory>,
/// This is an integer number between 0 and 100 (both inclusive) that
/// reports the completeness.
@@ -134,3 +125,4 @@ pub struct ListSessionResultDetailsResponse {
pub maybe_successfully_completed_at: Option<DateTime<Utc>>,
}
@@ -1,4 +1,3 @@
pub mod list_user_wallets;
pub mod moderator_add_banked_balance_to_wallet;
pub mod moderator_create_wallet_for_user;
pub mod moderator_get_wallet;
@@ -1,19 +0,0 @@
use enums::common::payments_namespace::PaymentsNamespace;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use tokens::tokens::wallets::WalletToken;
use utoipa::ToSchema;
pub const MODERATOR_CREATE_WALLET_FOR_USER_PATH: &str = "/v1/moderation/wallet/create_for_user";
#[derive(Deserialize, ToSchema)]
pub struct ModeratorCreateWalletForUserRequest {
pub user_token: Option<UserToken>,
pub payments_namespace: Option<PaymentsNamespace>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorCreateWalletForUserResponse {
pub success: bool,
pub wallet_token: WalletToken,
}
@@ -1,3 +1,2 @@
pub mod image;
pub mod splat;
pub mod video;
@@ -1,20 +0,0 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::cost_estimate::estimate_splat_cost::{
EstimateSplatCostRequest, EstimateSplatCostResponse, ESTIMATE_SPLAT_COST_PATH,
};
pub async fn estimate_splat_cost(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: EstimateSplatCostRequest,
) -> Result<EstimateSplatCostResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
ESTIMATE_SPLAT_COST_PATH,
maybe_creds,
request,
).await?)
}
@@ -1 +0,0 @@
pub mod estimate_splat_cost;
@@ -1,21 +0,0 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
Flux2LoraEditImageAngleRequest, Flux2LoraEditImageAngleResponse,
FLUX_2_LORA_EDIT_IMAGE_ANGLE_PATH,
};
pub async fn flux_2_lora_edit_image_angle(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: Flux2LoraEditImageAngleRequest,
) -> Result<Flux2LoraEditImageAngleResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
FLUX_2_LORA_EDIT_IMAGE_ANGLE_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,2 +0,0 @@
pub mod flux_2_lora_edit_image_angle;
pub mod qwen_edit_2511_edit_image_angle;
@@ -1,21 +0,0 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::{
QwenEdit2511EditImageAngleRequest, QwenEdit2511EditImageAngleResponse,
QWEN_EDIT_2511_EDIT_IMAGE_ANGLE_PATH,
};
pub async fn qwen_edit_2511_edit_image_angle(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: QwenEdit2511EditImageAngleRequest,
) -> Result<QwenEdit2511EditImageAngleResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
QWEN_EDIT_2511_EDIT_IMAGE_ANGLE_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,4 +1,3 @@
pub mod angle;
pub mod bg_removal;
pub mod edit;
pub mod inpaint;
@@ -1,5 +1,4 @@
pub mod cost_estimate;
pub mod image;
pub mod object;
pub mod splat;
pub mod video;
@@ -1,18 +0,0 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::splat::generate_worldlabs_marble_0p1_mini_splat::{GenerateWorldlabsMarble0p1MiniSplatRequest, GenerateWorldlabsMarble0p1MiniSplatResponse, GENERATE_WORLDLABS_MARBLE_0P1_MINI_SPLAT_URL_PATH};
pub async fn generate_worldlabs_marble_0p1_mini_splat(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateWorldlabsMarble0p1MiniSplatRequest,
) -> Result<GenerateWorldlabsMarble0p1MiniSplatResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
GENERATE_WORLDLABS_MARBLE_0P1_MINI_SPLAT_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,18 +0,0 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::splat::generate_worldlabs_marble_0p1_plus_splat::{GenerateWorldlabsMarble0p1PlusSplatRequest, GenerateWorldlabsMarble0p1PlusSplatResponse, GENERATE_WORLDLABS_MARBLE_0P1_PLUS_SPLAT_URL_PATH};
pub async fn generate_worldlabs_marble_0p1_plus_splat(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateWorldlabsMarble0p1PlusSplatRequest,
) -> Result<GenerateWorldlabsMarble0p1PlusSplatResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
GENERATE_WORLDLABS_MARBLE_0P1_PLUS_SPLAT_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,2 +0,0 @@
pub mod generate_worldlabs_marble_0p1_mini_splat;
pub mod generate_worldlabs_marble_0p1_plus_splat;
@@ -1,18 +0,0 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::{Kling3p0ProMultiFunctionVideoGenRequest, Kling3p0ProMultiFunctionVideoGenResponse, KLING_3P0_PRO_MULTI_FUNCTION_VIDEO_PATH};
pub async fn kling_3p0_pro_multi_function_video_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: Kling3p0ProMultiFunctionVideoGenRequest,
) -> Result<Kling3p0ProMultiFunctionVideoGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
KLING_3P0_PRO_MULTI_FUNCTION_VIDEO_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,18 +0,0 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::{Kling3p0StandardMultiFunctionVideoGenRequest, Kling3p0StandardMultiFunctionVideoGenResponse, KLING_3P0_STANDARD_MULTI_FUNCTION_VIDEO_PATH};
pub async fn kling_3p0_standard_multi_function_video_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: Kling3p0StandardMultiFunctionVideoGenRequest,
) -> Result<Kling3p0StandardMultiFunctionVideoGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
KLING_3P0_STANDARD_MULTI_FUNCTION_VIDEO_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,9 +1,6 @@
pub mod kling_2p5_turbo_pro_multi_function_video_gen;
pub mod seedance_1p5_pro_multi_function_video_gen;
pub mod seedance_2p0_multi_function_video_gen;
pub mod kling_2p6_pro_multi_function_video_gen;
pub mod kling_3p0_pro_multi_function_video_gen;
pub mod kling_3p0_standard_multi_function_video_gen;
pub mod sora_2_multi_function_video_gen;
pub mod sora_2_pro_multi_function_video_gen;
pub mod veo_3p1_fast_multi_function_video_gen;
@@ -1,18 +0,0 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::{Seedance1p5ProMultiFunctionVideoGenRequest, Seedance1p5ProMultiFunctionVideoGenResponse, SEEDANCE_1P5_PRO_MULTI_FUNCTION_VIDEO_GEN_PATH};
pub async fn seedance_1p5_pro_multi_function_video_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: Seedance1p5ProMultiFunctionVideoGenRequest,
) -> Result<Seedance1p5ProMultiFunctionVideoGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
SEEDANCE_1P5_PRO_MULTI_FUNCTION_VIDEO_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -17,11 +17,10 @@ workspace-build-acceleration.workspace = true
# Internal
artcraft_api_defs.workspace = true
artcraft_client.workspace = true
fal_client.workspace = true
seedance2pro.workspace = true
artcraft_client.workspace = true
tokens.workspace = true
world_labs_api.workspace = true
# External
anyhow.workspace = true
@@ -27,8 +27,4 @@ pub enum CommonImageModel {
Seedream4p5,
#[serde(rename = "seedream_5_lite")]
Seedream5Lite,
#[serde(rename = "qwen_edit_2511_angles")]
QwenEdit2511Angles,
#[serde(rename = "flux_2_lora_angles")]
Flux2LoraAngles,
}
@@ -1,13 +0,0 @@
use serde_derive::{Deserialize, Serialize};
/// Common splat models supported by the router.
/// Not all models are available through all providers.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommonSplatModel {
#[serde(rename = "marble_0p1_mini")]
Marble0p1Mini,
#[serde(rename = "marble_0p1_plus")]
Marble0p1Plus,
}
@@ -23,18 +23,9 @@ pub enum CommonVideoModel {
#[serde(rename = "kling_2p6_pro")]
Kling2p6Pro,
#[serde(rename = "kling_3p0_standard")]
Kling3p0Standard,
#[serde(rename = "kling_3p0_pro")]
Kling3p0Pro,
#[serde(rename = "seedance_1p0_lite")]
Seedance10Lite,
#[serde(rename = "seedance_1p5_pro")]
Seedance1p5Pro,
#[serde(rename = "seedance_2p0")]
Seedance2p0,
@@ -1,7 +1,6 @@
pub mod common_aspect_ratio;
pub mod common_image_model;
pub mod common_resolution;
pub mod common_splat_model;
pub mod common_video_model;
pub mod image_list_ref;
pub mod image_ref;
@@ -47,9 +47,6 @@ mod tests {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
@@ -35,9 +35,6 @@ mod tests {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
let estimate = request.build()
.expect("build should succeed")
@@ -1,72 +0,0 @@
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::Flux2LoraEditImageAngleNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
pub(crate) fn estimate_image_cost_artcraft_flux_2_lora_angles(
plan: &PlanArtcraftFlux2LoraAngles<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 4;
let num_images: u64 = match plan.num_images {
Flux2LoraEditImageAngleNumImages::One => 1,
Flux2LoraEditImageAngleNumImages::Two => 2,
Flux2LoraEditImageAngleNumImages::Three => 3,
Flux2LoraEditImageAngleNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * num_images;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use tokens::tokens::media_files::MediaFileToken;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let tokens = vec![MediaFileToken::new_from_str("test_token")];
let request = GenerateImageRequest {
model: CommonImageModel::Flux2LoraAngles,
provider: Provider::Artcraft,
prompt: None,
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
resolution: None,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
horizontal_angle: None,
vertical_angle: None,
zoom: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.04/image = 4 cents each
assert_eq!(estimate_usd_cents(1), 4);
assert_eq!(estimate_usd_cents(2), 8);
assert_eq!(estimate_usd_cents(3), 12);
assert_eq!(estimate_usd_cents(4), 16);
}
}
@@ -47,9 +47,6 @@ mod tests {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
@@ -47,9 +47,6 @@ mod tests {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
@@ -86,9 +86,6 @@ mod tests {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
@@ -47,9 +47,6 @@ mod tests {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
@@ -55,9 +55,6 @@ mod tests {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
@@ -54,9 +54,6 @@ mod tests {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
@@ -1,72 +0,0 @@
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::QwenEdit2511EditImageAngleNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
pub(crate) fn estimate_image_cost_artcraft_qwen_edit_2511_angles(
plan: &PlanArtcraftQwenEdit2511Angles<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 4;
let num_images: u64 = match plan.num_images {
QwenEdit2511EditImageAngleNumImages::One => 1,
QwenEdit2511EditImageAngleNumImages::Two => 2,
QwenEdit2511EditImageAngleNumImages::Three => 3,
QwenEdit2511EditImageAngleNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * num_images;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use tokens::tokens::media_files::MediaFileToken;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let tokens = vec![MediaFileToken::new_from_str("test_token")];
let request = GenerateImageRequest {
model: CommonImageModel::QwenEdit2511Angles,
provider: Provider::Artcraft,
prompt: None,
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
resolution: None,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.04/image = 4 cents each
assert_eq!(estimate_usd_cents(1), 4);
assert_eq!(estimate_usd_cents(2), 8);
assert_eq!(estimate_usd_cents(3), 12);
assert_eq!(estimate_usd_cents(4), 16);
}
}
@@ -47,9 +47,6 @@ mod tests {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
@@ -47,9 +47,6 @@ mod tests {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
@@ -47,9 +47,6 @@ mod tests {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
@@ -9,5 +9,3 @@ pub mod estimate_image_cost_artcraft_nano_banana_pro;
pub mod estimate_image_cost_artcraft_seedream_4;
pub mod estimate_image_cost_artcraft_seedream_4p5;
pub mod estimate_image_cost_artcraft_seedream_5_lite;
pub mod estimate_image_cost_artcraft_qwen_edit_2511_angles;
pub mod estimate_image_cost_artcraft_flux_2_lora_angles;
@@ -52,9 +52,6 @@ mod tests {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
@@ -1,72 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
ArtcraftImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::Flux2LoraEditImageAngleRequest;
use artcraft_client::endpoints::generate::image::angle::flux_2_lora_edit_image_angle::flux_2_lora_edit_image_angle;
pub async fn execute_artcraft_flux_2_lora_angles(
plan: &PlanArtcraftFlux2LoraAngles<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = Flux2LoraEditImageAngleRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
image_media_token: plan.image_input.clone(),
horizontal_angle: plan.horizontal_angle,
vertical_angle: plan.vertical_angle,
zoom: plan.zoom,
num_images: Some(plan.num_images),
image_size: plan.image_size,
};
let response = flux_2_lora_edit_image_angle(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_flux_2_lora_angles_image_request, get_artcraft_client};
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_angle_edit_flux_2_lora() {
let client = get_artcraft_client();
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::SquareHd),
image_batch_count: Some(1),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
horizontal_angle: Some(30.0),
vertical_angle: Some(0.0),
zoom: Some(1.0),
..base_flux_2_lora_angles_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -1,74 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
ArtcraftImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::QwenEdit2511EditImageAngleRequest;
use artcraft_client::endpoints::generate::image::angle::qwen_edit_2511_edit_image_angle::qwen_edit_2511_edit_image_angle;
pub async fn execute_artcraft_qwen_edit_2511_angles(
plan: &PlanArtcraftQwenEdit2511Angles<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = QwenEdit2511EditImageAngleRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
image_media_token: plan.image_input.clone(),
horizontal_angle: plan.horizontal_angle,
vertical_angle: plan.vertical_angle,
zoom: plan.zoom,
additional_prompt: plan.prompt.map(|p| p.to_string()),
num_images: Some(plan.num_images),
image_size: plan.image_size,
};
let response = qwen_edit_2511_edit_image_angle(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_qwen_edit_2511_angles_image_request, get_artcraft_client};
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_angle_edit_qwen_edit_2511() {
let client = get_artcraft_client();
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::SquareHd),
image_batch_count: Some(1),
prompt: None,
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
horizontal_angle: Some(30.0),
vertical_angle: Some(0.0),
zoom: Some(1.0),
..base_qwen_edit_2511_angles_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -9,5 +9,3 @@ pub mod generate_image_artcraft_nano_banana_pro;
pub mod generate_image_artcraft_seedream_4;
pub mod generate_image_artcraft_seedream_4p5;
pub mod generate_image_artcraft_seedream_5_lite;
pub mod generate_image_artcraft_qwen_edit_2511_angles;
pub mod generate_image_artcraft_flux_2_lora_angles;
@@ -19,8 +19,6 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::plan_generate_image_artcraft_seedream_4;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::plan_generate_image_artcraft_seedream_4p5;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::plan_generate_image_artcraft_seedream_5_lite;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::plan_generate_image_artcraft_qwen_edit_2511_angles;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::plan_generate_image_artcraft_flux_2_lora_angles;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::plan_generate_image_fal_nano_banana_pro;
pub struct GenerateImageRequest<'a> {
@@ -47,15 +45,6 @@ pub struct GenerateImageRequest<'a> {
/// How many images to generate.
pub image_batch_count: Option<u16>,
/// Only for angle manipulation models.
pub horizontal_angle: Option<f64>,
/// Only for angle manipulation models.
pub vertical_angle: Option<f64>,
/// Only for angle manipulation models.
pub zoom: Option<f64>,
/// If the request is a mismatch with the (model/provider), how to mitigate it.
pub request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy,
@@ -107,12 +96,6 @@ impl<'a> GenerateImageRequest<'a> {
CommonImageModel::Seedream5Lite => {
plan_generate_image_artcraft_seedream_5_lite(self).map(ImageGenerationPlan::ArtcraftSeedream5Lite)
}
CommonImageModel::QwenEdit2511Angles => {
plan_generate_image_artcraft_qwen_edit_2511_angles(self).map(ImageGenerationPlan::ArtcraftQwenEdit2511Angles)
}
CommonImageModel::Flux2LoraAngles => {
plan_generate_image_artcraft_flux_2_lora_angles(self).map(ImageGenerationPlan::ArtcraftFlux2LoraAngles)
}
},
Provider::Fal => match self.model {
CommonImageModel::NanaBananaPro => {
@@ -127,9 +110,7 @@ impl<'a> GenerateImageRequest<'a> {
| CommonImageModel::NanaBanana2
| CommonImageModel::Seedream4
| CommonImageModel::Seedream4p5
| CommonImageModel::Seedream5Lite
| CommonImageModel::QwenEdit2511Angles
| CommonImageModel::Flux2LoraAngles => {
| CommonImageModel::Seedream5Lite => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "provider",
value: format!("{:?} is only available on the Artcraft provider", self.model),
@@ -11,8 +11,6 @@ use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraf
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4::estimate_image_cost_artcraft_seedream_4;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4p5::estimate_image_cost_artcraft_seedream_4p5;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_5_lite::estimate_image_cost_artcraft_seedream_5_lite;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_qwen_edit_2511_angles::estimate_image_cost_artcraft_qwen_edit_2511_angles;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_2_lora_angles::estimate_image_cost_artcraft_flux_2_lora_angles;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_pro::estimate_image_cost_fal_nano_banana_pro;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_dev::execute_artcraft_flux_1_dev;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_schnell::execute_artcraft_flux_1_schnell;
@@ -25,8 +23,6 @@ use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4::execute_artcraft_seedream_4;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4p5::execute_artcraft_seedream_4p5;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_5_lite::execute_artcraft_seedream_5_lite;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_qwen_edit_2511_angles::execute_artcraft_qwen_edit_2511_angles;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_2_lora_angles::execute_artcraft_flux_2_lora_angles;
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_pro::execute_fal_nano_banana_pro;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
@@ -41,8 +37,6 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::PlanArtcraftSeedream4;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::PlanArtcraftSeedream4p5;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::PlanFalNanaBananaPro;
#[derive(Debug)]
@@ -58,8 +52,6 @@ pub enum ImageGenerationPlan<'a> {
ArtcraftSeedream4(PlanArtcraftSeedream4<'a>),
ArtcraftSeedream4p5(PlanArtcraftSeedream4p5<'a>),
ArtcraftSeedream5Lite(PlanArtcraftSeedream5Lite<'a>),
ArtcraftQwenEdit2511Angles(PlanArtcraftQwenEdit2511Angles<'a>),
ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles<'a>),
FalNanaBananaPro(PlanFalNanaBananaPro<'a>),
}
@@ -113,14 +105,6 @@ impl<'a> ImageGenerationPlan<'a> {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_seedream_5_lite(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_qwen_edit_2511_angles(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_flux_2_lora_angles(plan, artcraft_client).await
}
ImageGenerationPlan::FalNanaBananaPro(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_nano_banana_pro(plan, fal_client).await
@@ -163,12 +147,6 @@ impl<'a> ImageGenerationPlan<'a> {
ImageGenerationPlan::ArtcraftSeedream5Lite(plan) => {
estimate_image_cost_artcraft_seedream_5_lite(plan)
}
ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) => {
estimate_image_cost_artcraft_qwen_edit_2511_angles(plan)
}
ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) => {
estimate_image_cost_artcraft_flux_2_lora_angles(plan)
}
ImageGenerationPlan::FalNanaBananaPro(plan) => {
estimate_image_cost_fal_nano_banana_pro(plan)
}
@@ -9,5 +9,3 @@ pub mod plan_generate_image_artcraft_nano_banana_pro;
pub mod plan_generate_image_artcraft_seedream_4;
pub mod plan_generate_image_artcraft_seedream_4p5;
pub mod plan_generate_image_artcraft_seedream_5_lite;
pub mod plan_generate_image_artcraft_qwen_edit_2511_angles;
pub mod plan_generate_image_artcraft_flux_2_lora_angles;
@@ -1,254 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
Flux2LoraEditImageAngleImageSize,
Flux2LoraEditImageAngleNumImages,
};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftFlux2LoraAngles<'a> {
pub image_input: &'a MediaFileToken,
pub horizontal_angle: Option<f64>,
pub vertical_angle: Option<f64>,
pub zoom: Option<f64>,
pub image_size: Option<Flux2LoraEditImageAngleImageSize>,
pub num_images: Flux2LoraEditImageAngleNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_flux_2_lora_angles<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftFlux2LoraAngles<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
// Angle models require exactly one input image.
let image_input = resolve_single_image_input(request.image_inputs)?;
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftFlux2LoraAngles {
image_input,
horizontal_angle: request.horizontal_angle,
vertical_angle: request.vertical_angle,
zoom: request.zoom,
image_size,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}
fn resolve_single_image_input<'a>(
image_list_ref: Option<ImageListRef<'a>>,
) -> Result<&'a MediaFileToken, ArtcraftRouterError> {
match image_list_ref {
None => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "Angle models require exactly one input image".to_string(),
}))
}
Some(ImageListRef::MediaFileTokens(tokens)) => {
if tokens.len() != 1 {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: format!("Angle models require exactly one input image, got {}", tokens.len()),
}));
}
Ok(&tokens[0])
}
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
// Flux 2 LoRA supported image sizes:
// Square, SquareHd
// PortraitFourThree, PortraitSixteenNine
// LandscapeFourThree, LandscapeSixteenNine
fn plan_image_size(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<Flux2LoraEditImageAngleImageSize>, ArtcraftRouterError> {
use Flux2LoraEditImageAngleImageSize as S;
match aspect_ratio {
None => Ok(None),
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto4k) => {
Ok(Some(S::SquareHd))
}
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine)
| Some(unsupported @ CommonAspectRatio::TallFourByFive)
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::SquareHd)),
},
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Flux2LoraEditImageAngleNumImages, ArtcraftRouterError> {
use Flux2LoraEditImageAngleNumImages as N;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(N::One),
2 => Ok(N::Two),
3 => Ok(N::Three),
4 => Ok(N::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(N::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_flux_2_lora_angles_image_request;
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
Flux2LoraEditImageAngleImageSize as S,
Flux2LoraEditImageAngleNumImages as N,
};
use tokens::tokens::media_files::MediaFileToken;
fn make_token() -> MediaFileToken {
MediaFileToken::new_from_str("test_token_123")
}
#[test]
fn requires_image_input() {
let request = GenerateImageRequest {
image_inputs: None,
..base_flux_2_lora_angles_image_request()
};
let result = request.build();
assert!(result.is_err());
}
#[test]
fn rejects_urls() {
let urls = vec!["https://example.com/img.png".to_string()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
..base_flux_2_lora_angles_image_request()
};
let result = request.build();
assert!(result.is_err());
}
#[test]
fn accepts_single_media_token() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_flux_2_lora_angles_image_request()
};
let result = request.build();
assert!(result.is_ok());
}
#[test]
fn rejects_multiple_media_tokens() {
let tokens = vec![make_token(), make_token()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_flux_2_lora_angles_image_request()
};
let result = request.build();
assert!(result.is_err());
}
#[test]
fn image_size_none_is_none() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
aspect_ratio: None,
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_flux_2_lora_angles_image_request()
};
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
assert!(plan.image_size.is_none());
}
#[test]
fn image_size_square_maps_directly() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_flux_2_lora_angles_image_request()
};
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
assert!(matches!(plan.image_size, Some(S::Square)));
}
#[test]
fn num_images_direct_mapping() {
let tokens = vec![make_token()];
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest {
image_batch_count: Some(count),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_flux_2_lora_angles_image_request()
};
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
"expected {:?} for count {}", expected, count,
);
}
}
#[test]
fn angle_fields_are_passed_through() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
horizontal_angle: Some(45.0),
vertical_angle: Some(-30.0),
zoom: Some(1.5),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_flux_2_lora_angles_image_request()
};
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
assert_eq!(plan.horizontal_angle, Some(45.0));
assert_eq!(plan.vertical_angle, Some(-30.0));
assert_eq!(plan.zoom, Some(1.5));
}
}
@@ -1,256 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::{
QwenEdit2511EditImageAngleImageSize,
QwenEdit2511EditImageAngleNumImages,
};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftQwenEdit2511Angles<'a> {
pub prompt: Option<&'a str>,
pub image_input: &'a MediaFileToken,
pub horizontal_angle: Option<f64>,
pub vertical_angle: Option<f64>,
pub zoom: Option<f64>,
pub image_size: Option<QwenEdit2511EditImageAngleImageSize>,
pub num_images: QwenEdit2511EditImageAngleNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_qwen_edit_2511_angles<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftQwenEdit2511Angles<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
// Angle models require exactly one input image.
let image_input = resolve_single_image_input(request.image_inputs)?;
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftQwenEdit2511Angles {
prompt: request.prompt,
image_input,
horizontal_angle: request.horizontal_angle,
vertical_angle: request.vertical_angle,
zoom: request.zoom,
image_size,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}
fn resolve_single_image_input<'a>(
image_list_ref: Option<ImageListRef<'a>>,
) -> Result<&'a MediaFileToken, ArtcraftRouterError> {
match image_list_ref {
None => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "Angle models require exactly one input image".to_string(),
}))
}
Some(ImageListRef::MediaFileTokens(tokens)) => {
if tokens.len() != 1 {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: format!("Angle models require exactly one input image, got {}", tokens.len()),
}));
}
Ok(&tokens[0])
}
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
// Qwen Edit 2511 supported image sizes:
// Square, SquareHd
// PortraitFourThree, PortraitSixteenNine
// LandscapeFourThree, LandscapeSixteenNine
fn plan_image_size(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<QwenEdit2511EditImageAngleImageSize>, ArtcraftRouterError> {
use QwenEdit2511EditImageAngleImageSize as S;
match aspect_ratio {
None => Ok(None),
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto4k) => {
Ok(Some(S::SquareHd))
}
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine)
| Some(unsupported @ CommonAspectRatio::TallFourByFive)
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::SquareHd)),
},
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<QwenEdit2511EditImageAngleNumImages, ArtcraftRouterError> {
use QwenEdit2511EditImageAngleNumImages as N;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(N::One),
2 => Ok(N::Two),
3 => Ok(N::Three),
4 => Ok(N::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(N::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_qwen_edit_2511_angles_image_request;
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::{
QwenEdit2511EditImageAngleImageSize as S,
QwenEdit2511EditImageAngleNumImages as N,
};
use tokens::tokens::media_files::MediaFileToken;
fn make_token() -> MediaFileToken {
MediaFileToken::new_from_str("test_token_123")
}
#[test]
fn requires_image_input() {
let request = GenerateImageRequest {
image_inputs: None,
..base_qwen_edit_2511_angles_image_request()
};
let result = request.build();
assert!(result.is_err());
}
#[test]
fn rejects_urls() {
let urls = vec!["https://example.com/img.png".to_string()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
..base_qwen_edit_2511_angles_image_request()
};
let result = request.build();
assert!(result.is_err());
}
#[test]
fn accepts_single_media_token() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_qwen_edit_2511_angles_image_request()
};
let result = request.build();
assert!(result.is_ok());
}
#[test]
fn rejects_multiple_media_tokens() {
let tokens = vec![make_token(), make_token()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_qwen_edit_2511_angles_image_request()
};
let result = request.build();
assert!(result.is_err());
}
#[test]
fn image_size_none_is_none() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
aspect_ratio: None,
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_qwen_edit_2511_angles_image_request()
};
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
assert!(plan.image_size.is_none());
}
#[test]
fn image_size_square_maps_directly() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_qwen_edit_2511_angles_image_request()
};
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
assert!(matches!(plan.image_size, Some(S::Square)));
}
#[test]
fn num_images_direct_mapping() {
let tokens = vec![make_token()];
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest {
image_batch_count: Some(count),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_qwen_edit_2511_angles_image_request()
};
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
"expected {:?} for count {}", expected, count,
);
}
}
#[test]
fn angle_fields_are_passed_through() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
horizontal_angle: Some(45.0),
vertical_angle: Some(-30.0),
zoom: Some(1.5),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_qwen_edit_2511_angles_image_request()
};
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
assert_eq!(plan.horizontal_angle, Some(45.0));
assert_eq!(plan.vertical_angle, Some(-30.0));
assert_eq!(plan.zoom, Some(1.5));
}
}
@@ -254,9 +254,6 @@ mod tests {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
}
}
@@ -1,76 +0,0 @@
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_mini::PlanArtcraftMarble0p1Mini;
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
use world_labs_api::api::api_types::world_labs_model::WorldLabsModel;
use world_labs_api::pricing::check_pricing::{calculate_cost, InputType};
pub(crate) fn estimate_splat_cost_artcraft_marble_0p1_mini(
plan: &PlanArtcraftMarble0p1Mini<'_>,
) -> SplatGenerationCostEstimate {
let input_type = if plan.reference_image.is_some() {
InputType::ImageNonPanorama
} else {
InputType::Text
};
let cost = calculate_cost(WorldLabsModel::Marble0p1Mini, input_type);
let cost_in_usd_cents = cost.us_dollar_cents as u64;
SplatGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_splat_model::CommonSplatModel;
use crate::api::provider::Provider;
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
use tokens::tokens::media_files::MediaFileToken;
fn estimate_usd_cents(prompt: Option<&str>, image_tokens: Option<&Vec<MediaFileToken>>) -> u64 {
let request = GenerateSplatRequest {
model: CommonSplatModel::Marble0p1Mini,
provider: Provider::Artcraft,
prompt,
reference_images: image_tokens.map(crate::api::image_list_ref::ImageListRef::MediaFileTokens),
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_text_only() {
let cost = estimate_usd_cents(Some("a room"), None);
assert_eq!(cost, 18); // 230 credits → 18 cents
}
#[test]
fn test_estimate_cost_no_image_no_prompt() {
let cost = estimate_usd_cents(None, None);
assert_eq!(cost, 18);
}
#[test]
fn test_estimate_cost_image() {
let tokens = vec![MediaFileToken("test_token".to_string())];
let cost = estimate_usd_cents(None, Some(&tokens));
assert_eq!(cost, 18); // 230 credits → 18 cents (same as text for mini)
}
#[test]
fn test_estimate_cost_image_with_prompt() {
let tokens = vec![MediaFileToken("test_token".to_string())];
let cost = estimate_usd_cents(Some("a cozy room"), Some(&tokens));
assert_eq!(cost, 18); // 230 credits → 18 cents
}
}
@@ -1,76 +0,0 @@
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_plus::PlanArtcraftMarble0p1Plus;
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
use world_labs_api::api::api_types::world_labs_model::WorldLabsModel;
use world_labs_api::pricing::check_pricing::{calculate_cost, InputType};
pub(crate) fn estimate_splat_cost_artcraft_marble_0p1_plus(
plan: &PlanArtcraftMarble0p1Plus<'_>,
) -> SplatGenerationCostEstimate {
let input_type = if plan.reference_image.is_some() {
InputType::ImageNonPanorama
} else {
InputType::Text
};
let cost = calculate_cost(WorldLabsModel::Marble0p1Plus, input_type);
let cost_in_usd_cents = cost.us_dollar_cents as u64;
SplatGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_splat_model::CommonSplatModel;
use crate::api::provider::Provider;
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
use tokens::tokens::media_files::MediaFileToken;
fn estimate_usd_cents(prompt: Option<&str>, image_tokens: Option<&Vec<MediaFileToken>>) -> u64 {
let request = GenerateSplatRequest {
model: CommonSplatModel::Marble0p1Plus,
provider: Provider::Artcraft,
prompt,
reference_images: image_tokens.map(crate::api::image_list_ref::ImageListRef::MediaFileTokens),
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_text_only() {
let cost = estimate_usd_cents(Some("a room"), None);
assert_eq!(cost, 126); // 1580 credits → 126 cents
}
#[test]
fn test_estimate_cost_no_image_no_prompt() {
let cost = estimate_usd_cents(None, None);
assert_eq!(cost, 126);
}
#[test]
fn test_estimate_cost_image() {
let tokens = vec![MediaFileToken("test_token".to_string())];
let cost = estimate_usd_cents(None, Some(&tokens));
assert_eq!(cost, 126); // 1580 credits → 126 cents (same as text for plus)
}
#[test]
fn test_estimate_cost_image_with_prompt() {
let tokens = vec![MediaFileToken("test_token".to_string())];
let cost = estimate_usd_cents(Some("a cozy room"), Some(&tokens));
assert_eq!(cost, 126); // 1580 credits → 126 cents
}
}
@@ -1,2 +0,0 @@
pub mod estimate_splat_cost_artcraft_marble_0p1_mini;
pub mod estimate_splat_cost_artcraft_marble_0p1_plus;
@@ -1 +0,0 @@
pub(crate) mod artcraft;
@@ -1,32 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_mini::PlanArtcraftMarble0p1Mini;
use crate::generate::generate_splat::generate_splat_response::{
ArtcraftSplatResponsePayload, GenerateSplatResponse,
};
use artcraft_api_defs::generate::splat::generate_worldlabs_marble_0p1_mini_splat::GenerateWorldlabsMarble0p1MiniSplatRequest;
use artcraft_client::endpoints::generate::splat::generate_worldlabs_marble_0p1_mini_splat::generate_worldlabs_marble_0p1_mini_splat;
pub async fn execute_artcraft_marble_0p1_mini(
plan: &PlanArtcraftMarble0p1Mini<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
let request = GenerateWorldlabsMarble0p1MiniSplatRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
image_media_file_token: plan.reference_image.cloned(),
prompt: plan.prompt.map(|p| p.to_string()),
};
let response = generate_worldlabs_marble_0p1_mini_splat(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateSplatResponse::Artcraft(ArtcraftSplatResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
@@ -1,32 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_plus::PlanArtcraftMarble0p1Plus;
use crate::generate::generate_splat::generate_splat_response::{
ArtcraftSplatResponsePayload, GenerateSplatResponse,
};
use artcraft_api_defs::generate::splat::generate_worldlabs_marble_0p1_plus_splat::GenerateWorldlabsMarble0p1PlusSplatRequest;
use artcraft_client::endpoints::generate::splat::generate_worldlabs_marble_0p1_plus_splat::generate_worldlabs_marble_0p1_plus_splat;
pub async fn execute_artcraft_marble_0p1_plus(
plan: &PlanArtcraftMarble0p1Plus<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
let request = GenerateWorldlabsMarble0p1PlusSplatRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
image_media_file_token: plan.reference_image.cloned(),
prompt: plan.prompt.map(|p| p.to_string()),
};
let response = generate_worldlabs_marble_0p1_plus_splat(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateSplatResponse::Artcraft(ArtcraftSplatResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
@@ -1,2 +0,0 @@
pub mod generate_splat_artcraft_marble_0p1_mini;
pub mod generate_splat_artcraft_marble_0p1_plus;
@@ -1 +0,0 @@
pub mod artcraft;
@@ -1,49 +0,0 @@
use crate::api::common_splat_model::CommonSplatModel;
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_mini::plan_generate_splat_artcraft_marble_0p1_mini;
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_plus::plan_generate_splat_artcraft_marble_0p1_plus;
use crate::generate::generate_splat::splat_generation_plan::SplatGenerationPlan;
pub struct GenerateSplatRequest<'a> {
/// Which model to use.
pub model: CommonSplatModel,
/// Which provider to use.
pub provider: Provider,
/// The prompt for splat generation (optional).
pub prompt: Option<&'a str>,
/// Reference images (optional).
pub reference_images: Option<ImageListRef<'a>>,
/// Some providers support idempotency.
/// If not supplied, we'll generate one for the required providers.
pub idempotency_token: Option<&'a str>,
}
impl<'a> GenerateSplatRequest<'a> {
/// Read the splat generation request, construct a plan, then yield a means to execute it.
pub fn build(&self) -> Result<SplatGenerationPlan<'_>, ArtcraftRouterError> {
match self.provider {
Provider::Artcraft => match self.model {
CommonSplatModel::Marble0p1Mini => {
plan_generate_splat_artcraft_marble_0p1_mini(self).map(SplatGenerationPlan::ArtcraftMarble0p1Mini)
}
CommonSplatModel::Marble0p1Plus => {
plan_generate_splat_artcraft_marble_0p1_plus(self).map(SplatGenerationPlan::ArtcraftMarble0p1Plus)
}
},
Provider::Fal => Err(ArtcraftRouterError::UnsupportedModel(
format!("Splat generation via Fal is not yet supported (model: {:?})", self.model)
)),
}
}
pub fn get_or_generate_idempotency_token(&self) -> String {
self.idempotency_token.map(|t| t.to_string())
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
}
}
@@ -1,19 +0,0 @@
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
#[derive(Clone, Debug)]
pub struct ArtcraftSplatResponsePayload {
pub inference_job_token: InferenceJobToken,
}
#[derive(Clone, Debug)]
pub enum GenerateSplatResponse {
Artcraft(ArtcraftSplatResponsePayload),
}
impl GenerateSplatResponse {
pub fn get_artcraft_payload(&self) -> Option<ArtcraftSplatResponsePayload> {
match self {
Self::Artcraft(p) => Some(p.clone()),
}
}
}
@@ -1,7 +0,0 @@
pub mod cost;
pub mod execute;
pub mod generate_splat_request;
pub mod generate_splat_response;
pub mod plan;
pub mod splat_generation_cost_estimate;
pub mod splat_generation_plan;
@@ -1,2 +0,0 @@
pub mod plan_generate_splat_artcraft_marble_0p1_mini;
pub mod plan_generate_splat_artcraft_marble_0p1_plus;
@@ -1,38 +0,0 @@
use crate::api::image_list_ref::ImageListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftMarble0p1Mini<'a> {
pub prompt: Option<&'a str>,
pub reference_image: Option<&'a MediaFileToken>,
pub idempotency_token: String,
}
pub fn plan_generate_splat_artcraft_marble_0p1_mini<'a>(
request: &'a GenerateSplatRequest<'a>,
) -> Result<PlanArtcraftMarble0p1Mini<'a>, ArtcraftRouterError> {
let reference_image = resolve_single_image_ref(request.reference_images)?;
Ok(PlanArtcraftMarble0p1Mini {
prompt: request.prompt,
reference_image,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}
fn resolve_single_image_ref<'a>(
image_list_ref: Option<ImageListRef<'a>>,
) -> Result<Option<&'a MediaFileToken>, ArtcraftRouterError> {
match image_list_ref {
None => Ok(None),
Some(ImageListRef::MediaFileTokens(tokens)) => {
Ok(tokens.first())
}
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
@@ -1,38 +0,0 @@
use crate::api::image_list_ref::ImageListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftMarble0p1Plus<'a> {
pub prompt: Option<&'a str>,
pub reference_image: Option<&'a MediaFileToken>,
pub idempotency_token: String,
}
pub fn plan_generate_splat_artcraft_marble_0p1_plus<'a>(
request: &'a GenerateSplatRequest<'a>,
) -> Result<PlanArtcraftMarble0p1Plus<'a>, ArtcraftRouterError> {
let reference_image = resolve_single_image_ref(request.reference_images)?;
Ok(PlanArtcraftMarble0p1Plus {
prompt: request.prompt,
reference_image,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}
fn resolve_single_image_ref<'a>(
image_list_ref: Option<ImageListRef<'a>>,
) -> Result<Option<&'a MediaFileToken>, ArtcraftRouterError> {
match image_list_ref {
None => Ok(None),
Some(ImageListRef::MediaFileTokens(tokens)) => {
Ok(tokens.first())
}
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
@@ -1 +0,0 @@
pub mod artcraft;
@@ -1,9 +0,0 @@
/// The result of estimating the cost of a splat generation plan.
pub struct SplatGenerationCostEstimate {
pub cost_in_credits: Option<u64>,
pub cost_in_usd_cents: Option<u64>,
pub is_free: bool,
pub is_unlimited: bool,
pub is_rate_limited: bool,
pub has_watermark: bool,
}
@@ -1,45 +0,0 @@
use crate::client::router_client::RouterClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_splat::cost::artcraft::estimate_splat_cost_artcraft_marble_0p1_mini::estimate_splat_cost_artcraft_marble_0p1_mini;
use crate::generate::generate_splat::cost::artcraft::estimate_splat_cost_artcraft_marble_0p1_plus::estimate_splat_cost_artcraft_marble_0p1_plus;
use crate::generate::generate_splat::execute::artcraft::generate_splat_artcraft_marble_0p1_mini::execute_artcraft_marble_0p1_mini;
use crate::generate::generate_splat::execute::artcraft::generate_splat_artcraft_marble_0p1_plus::execute_artcraft_marble_0p1_plus;
use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse;
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_mini::PlanArtcraftMarble0p1Mini;
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_plus::PlanArtcraftMarble0p1Plus;
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
#[derive(Debug)]
pub enum SplatGenerationPlan<'a> {
ArtcraftMarble0p1Mini(PlanArtcraftMarble0p1Mini<'a>),
ArtcraftMarble0p1Plus(PlanArtcraftMarble0p1Plus<'a>),
}
impl<'a> SplatGenerationPlan<'a> {
pub async fn generate_splat(
&self,
client: &RouterClient,
) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
match self {
SplatGenerationPlan::ArtcraftMarble0p1Mini(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_marble_0p1_mini(plan, artcraft_client).await
}
SplatGenerationPlan::ArtcraftMarble0p1Plus(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_marble_0p1_plus(plan, artcraft_client).await
}
}
}
pub fn estimate_costs(&self) -> SplatGenerationCostEstimate {
match self {
SplatGenerationPlan::ArtcraftMarble0p1Mini(plan) => {
estimate_splat_cost_artcraft_marble_0p1_mini(plan)
}
SplatGenerationPlan::ArtcraftMarble0p1Plus(plan) => {
estimate_splat_cost_artcraft_marble_0p1_plus(plan)
}
}
}
}
@@ -1,98 +0,0 @@
use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::Kling3p0ProMultiFunctionVideoGenDuration;
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use fal_client::requests::webhook::video::text::enqueue_kling_3p0_pro_text_to_video_webhook::{
EnqueueKling3p0ProTextToVideoArgs,
EnqueueKling3p0ProTextToVideoDuration,
};
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_pro::PlanArtcraftKling3p0Pro;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
pub(crate) fn estimate_video_cost_artcraft_kling3p0_pro(
plan: &PlanArtcraftKling3p0Pro<'_>,
) -> VideoGenerationCostEstimate {
let duration = plan.duration
.map(|d| match d {
Kling3p0ProMultiFunctionVideoGenDuration::ThreeSeconds => EnqueueKling3p0ProTextToVideoDuration::ThreeSeconds,
Kling3p0ProMultiFunctionVideoGenDuration::FourSeconds => EnqueueKling3p0ProTextToVideoDuration::FourSeconds,
Kling3p0ProMultiFunctionVideoGenDuration::FiveSeconds => EnqueueKling3p0ProTextToVideoDuration::FiveSeconds,
Kling3p0ProMultiFunctionVideoGenDuration::SixSeconds => EnqueueKling3p0ProTextToVideoDuration::SixSeconds,
Kling3p0ProMultiFunctionVideoGenDuration::SevenSeconds => EnqueueKling3p0ProTextToVideoDuration::SevenSeconds,
Kling3p0ProMultiFunctionVideoGenDuration::EightSeconds => EnqueueKling3p0ProTextToVideoDuration::EightSeconds,
Kling3p0ProMultiFunctionVideoGenDuration::NineSeconds => EnqueueKling3p0ProTextToVideoDuration::NineSeconds,
Kling3p0ProMultiFunctionVideoGenDuration::TenSeconds => EnqueueKling3p0ProTextToVideoDuration::TenSeconds,
Kling3p0ProMultiFunctionVideoGenDuration::ElevenSeconds => EnqueueKling3p0ProTextToVideoDuration::ElevenSeconds,
Kling3p0ProMultiFunctionVideoGenDuration::TwelveSeconds => EnqueueKling3p0ProTextToVideoDuration::TwelveSeconds,
Kling3p0ProMultiFunctionVideoGenDuration::ThirteenSeconds => EnqueueKling3p0ProTextToVideoDuration::ThirteenSeconds,
Kling3p0ProMultiFunctionVideoGenDuration::FourteenSeconds => EnqueueKling3p0ProTextToVideoDuration::FourteenSeconds,
Kling3p0ProMultiFunctionVideoGenDuration::FifteenSeconds => EnqueueKling3p0ProTextToVideoDuration::FifteenSeconds,
});
// Use text-to-video cost calculator (same pricing for both modes)
let api_key = fal_client::creds::fal_api_key::FalApiKey::from_str("");
let args = EnqueueKling3p0ProTextToVideoArgs {
prompt: String::new(),
generate_audio: plan.generate_audio,
negative_prompt: None,
duration,
aspect_ratio: None,
shot_type: None,
webhook_url: "https://example.com",
api_key: &api_key,
};
let cost_in_usd_cents = args.calculate_cost_in_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_video_model::CommonVideoModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
fn estimate_usd_cents(duration_seconds: u16) -> u64 {
let request = GenerateVideoRequest {
model: CommonVideoModel::Kling3p0Pro,
provider: Provider::Artcraft,
prompt: None,
negative_prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
resolution: None,
aspect_ratio: None,
duration_seconds: Some(duration_seconds),
video_batch_count: None,
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// Kling 3.0 Pro: $0.336/sec (audio on, the default when None)
// Formula: (336 * duration_secs + 9) / 10 → ceiling division
// 5s: (336 * 5 + 9) / 10 = 1689 / 10 = 168
assert_eq!(estimate_usd_cents(5), 168);
// 10s: (336 * 10 + 9) / 10 = 3369 / 10 = 336
assert_eq!(estimate_usd_cents(10), 336);
// 15s: (336 * 15 + 9) / 10 = 5049 / 10 = 504
assert_eq!(estimate_usd_cents(15), 504);
}
}
@@ -1,98 +0,0 @@
use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::Kling3p0StandardMultiFunctionVideoGenDuration;
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use fal_client::requests::webhook::video::text::enqueue_kling_3p0_standard_text_to_video_webhook::{
EnqueueKling3p0StandardTextToVideoArgs,
EnqueueKling3p0StandardTextToVideoDuration,
};
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::PlanArtcraftKling3p0Standard;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
pub(crate) fn estimate_video_cost_artcraft_kling3p0_standard(
plan: &PlanArtcraftKling3p0Standard<'_>,
) -> VideoGenerationCostEstimate {
let duration = plan.duration
.map(|d| match d {
Kling3p0StandardMultiFunctionVideoGenDuration::ThreeSeconds => EnqueueKling3p0StandardTextToVideoDuration::ThreeSeconds,
Kling3p0StandardMultiFunctionVideoGenDuration::FourSeconds => EnqueueKling3p0StandardTextToVideoDuration::FourSeconds,
Kling3p0StandardMultiFunctionVideoGenDuration::FiveSeconds => EnqueueKling3p0StandardTextToVideoDuration::FiveSeconds,
Kling3p0StandardMultiFunctionVideoGenDuration::SixSeconds => EnqueueKling3p0StandardTextToVideoDuration::SixSeconds,
Kling3p0StandardMultiFunctionVideoGenDuration::SevenSeconds => EnqueueKling3p0StandardTextToVideoDuration::SevenSeconds,
Kling3p0StandardMultiFunctionVideoGenDuration::EightSeconds => EnqueueKling3p0StandardTextToVideoDuration::EightSeconds,
Kling3p0StandardMultiFunctionVideoGenDuration::NineSeconds => EnqueueKling3p0StandardTextToVideoDuration::NineSeconds,
Kling3p0StandardMultiFunctionVideoGenDuration::TenSeconds => EnqueueKling3p0StandardTextToVideoDuration::TenSeconds,
Kling3p0StandardMultiFunctionVideoGenDuration::ElevenSeconds => EnqueueKling3p0StandardTextToVideoDuration::ElevenSeconds,
Kling3p0StandardMultiFunctionVideoGenDuration::TwelveSeconds => EnqueueKling3p0StandardTextToVideoDuration::TwelveSeconds,
Kling3p0StandardMultiFunctionVideoGenDuration::ThirteenSeconds => EnqueueKling3p0StandardTextToVideoDuration::ThirteenSeconds,
Kling3p0StandardMultiFunctionVideoGenDuration::FourteenSeconds => EnqueueKling3p0StandardTextToVideoDuration::FourteenSeconds,
Kling3p0StandardMultiFunctionVideoGenDuration::FifteenSeconds => EnqueueKling3p0StandardTextToVideoDuration::FifteenSeconds,
});
// Use text-to-video cost calculator (same pricing for both modes)
let api_key = fal_client::creds::fal_api_key::FalApiKey::from_str("");
let args = EnqueueKling3p0StandardTextToVideoArgs {
prompt: String::new(),
generate_audio: plan.generate_audio,
negative_prompt: None,
duration,
aspect_ratio: None,
shot_type: None,
webhook_url: "https://example.com",
api_key: &api_key,
};
let cost_in_usd_cents = args.calculate_cost_in_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_video_model::CommonVideoModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
fn estimate_usd_cents(duration_seconds: u16) -> u64 {
let request = GenerateVideoRequest {
model: CommonVideoModel::Kling3p0Standard,
provider: Provider::Artcraft,
prompt: None,
negative_prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
resolution: None,
aspect_ratio: None,
duration_seconds: Some(duration_seconds),
video_batch_count: None,
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// Kling 3.0 Standard: $0.252/sec (audio on, the default when None)
// Formula: (252 * duration_secs + 9) / 10 → ceiling division
// 5s: (252 * 5 + 9) / 10 = 1269 / 10 = 126
assert_eq!(estimate_usd_cents(5), 126);
// 10s: (252 * 10 + 9) / 10 = 2529 / 10 = 252
assert_eq!(estimate_usd_cents(10), 252);
// 15s: (252 * 15 + 9) / 10 = 3789 / 10 = 378
assert_eq!(estimate_usd_cents(15), 378);
}
}
@@ -1,101 +0,0 @@
use artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::Seedance1p5ProMultiFunctionVideoGenDuration;
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use fal_client::requests::webhook::video::text::enqueue_seedance_1p5_pro_text_to_video_webhook::{
EnqueueSeedance1p5ProTextToVideoArgs,
EnqueueSeedance1p5ProTextToVideoDuration,
EnqueueSeedance1p5ProTextToVideoResolution,
};
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance1p5_pro::PlanArtcraftSeedance1p5Pro;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
pub(crate) fn estimate_video_cost_artcraft_seedance1p5_pro(
plan: &PlanArtcraftSeedance1p5Pro<'_>,
) -> VideoGenerationCostEstimate {
let duration = plan.duration
.map(|d| match d {
Seedance1p5ProMultiFunctionVideoGenDuration::FourSeconds => EnqueueSeedance1p5ProTextToVideoDuration::FourSeconds,
Seedance1p5ProMultiFunctionVideoGenDuration::FiveSeconds => EnqueueSeedance1p5ProTextToVideoDuration::FiveSeconds,
Seedance1p5ProMultiFunctionVideoGenDuration::SixSeconds => EnqueueSeedance1p5ProTextToVideoDuration::SixSeconds,
Seedance1p5ProMultiFunctionVideoGenDuration::SevenSeconds => EnqueueSeedance1p5ProTextToVideoDuration::SevenSeconds,
Seedance1p5ProMultiFunctionVideoGenDuration::EightSeconds => EnqueueSeedance1p5ProTextToVideoDuration::EightSeconds,
Seedance1p5ProMultiFunctionVideoGenDuration::NineSeconds => EnqueueSeedance1p5ProTextToVideoDuration::NineSeconds,
Seedance1p5ProMultiFunctionVideoGenDuration::TenSeconds => EnqueueSeedance1p5ProTextToVideoDuration::TenSeconds,
Seedance1p5ProMultiFunctionVideoGenDuration::ElevenSeconds => EnqueueSeedance1p5ProTextToVideoDuration::ElevenSeconds,
Seedance1p5ProMultiFunctionVideoGenDuration::TwelveSeconds => EnqueueSeedance1p5ProTextToVideoDuration::TwelveSeconds,
});
let resolution = plan.resolution
.map(|r| match r {
artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::Seedance1p5ProMultiFunctionVideoGenResolution::FourEightyP => EnqueueSeedance1p5ProTextToVideoResolution::FourEightyP,
artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::Seedance1p5ProMultiFunctionVideoGenResolution::SevenTwentyP => EnqueueSeedance1p5ProTextToVideoResolution::SevenTwentyP,
artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::Seedance1p5ProMultiFunctionVideoGenResolution::TenEightyP => EnqueueSeedance1p5ProTextToVideoResolution::TenEightyP,
});
// Use text-to-video cost calculator (same pricing for both modes)
let api_key = fal_client::creds::fal_api_key::FalApiKey::from_str("");
let args = EnqueueSeedance1p5ProTextToVideoArgs {
prompt: String::new(),
duration,
resolution,
aspect_ratio: None,
generate_audio: plan.generate_audio,
webhook_url: "https://example.com",
api_key: &api_key,
};
let cost_in_usd_cents = args.calculate_cost_in_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_video_model::CommonVideoModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
fn estimate_usd_cents(duration_seconds: u16) -> u64 {
let request = GenerateVideoRequest {
model: CommonVideoModel::Seedance1p5Pro,
provider: Provider::Artcraft,
prompt: None,
negative_prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
resolution: None,
aspect_ratio: None,
duration_seconds: Some(duration_seconds),
video_batch_count: None,
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// Default resolution is 720p (when None)
// 720p 5s = 26 cents (constant from Fal)
assert_eq!(estimate_usd_cents(5), 26);
// Other durations use token-based calculation
assert_eq!(estimate_usd_cents(4), 26);
assert_eq!(estimate_usd_cents(10), 65);
assert_eq!(estimate_usd_cents(12), 78);
}
}
@@ -57,7 +57,6 @@ mod tests {
model: CommonVideoModel::Seedance2p0,
provider: Provider::Artcraft,
prompt: None,
negative_prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
@@ -65,7 +64,6 @@ mod tests {
aspect_ratio: None,
duration_seconds: Some(duration_seconds),
video_batch_count: Some(video_batch_count),
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
};
@@ -1,4 +1 @@
pub mod estimate_video_cost_artcraft_kling3p0_pro;
pub mod estimate_video_cost_artcraft_kling3p0_standard;
pub mod estimate_video_cost_artcraft_seedance1p5_pro;
pub mod estimate_video_cost_artcraft_seedance2p0;

Some files were not shown because too many files have changed in this diff Show More