mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 17:28:24 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9ee643bf6 | ||
|
|
07e9a35044 |
@@ -1,3 +1,8 @@
|
||||
# Shell scripts and dotenv config files are executed/parsed on Linux (WSL2, CI)
|
||||
# even when the repo is cloned on Windows — they must stay LF in the worktree.
|
||||
*.sh text eol=lf
|
||||
*.env text eol=lf
|
||||
|
||||
# Ignore certain code for Github language statistics
|
||||
# https://github.com/github/linguist/blob/master/docs/overrides.md
|
||||
old_code/trumpet-trumped.com/dictionary/cmudict-0.7b linguist-vendored
|
||||
|
||||
@@ -104,3 +104,6 @@ windows/sqlite/sqlite3.lib
|
||||
crates/desktop/src-tauri/*.dll
|
||||
windows_dlls/
|
||||
|
||||
|
||||
# Portable local dev stack (script/bootstrap) - MySQL/Redis binaries, data, logs
|
||||
.devstack/
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE generic_inference_jobs\nSET\n status = ?,\n on_success_result_entity_type = ?,\n on_success_result_entity_token = ?,\n failure_reason = NULL,\n internal_debugging_failure_reason = NULL,\n success_execution_millis = ?,\n success_inference_execution_millis = ?,\n retry_at = NULL,\n successfully_completed_at = NOW()\nWHERE token = ?\nAND status NOT IN ('complete_success', 'complete_failure')\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "03919ba8286d18980ecaa0038df76f43f5388db0d3838181fa8f15f4cbb932af"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE generic_inference_jobs\nSET\n status = ?,\n assigned_worker = ?,\n last_assigned_worker = ?,\n assigned_cluster = ?,\n first_started_at = NOW()\nWHERE token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "258ac40d3fab39f110e1e82d50e73542423bfe9a21f8127e6cd83214c1726e25"
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`\nFROM generic_inference_jobs\nWHERE job_type = ?\nAND status = 'pending'\nORDER BY id ASC\nLIMIT 1\nFOR UPDATE SKIP LOCKED\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": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6ca0fcdf495624cf347f612586868f6dea93f52d9f942a92dcd2b3e6aba7c63c"
|
||||
}
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n users.token as `user_token: UserToken`,\n users.username,\n users.display_name,\n users.email_address,\n users.user_role_slug,\n users.is_banned,\n user_roles.can_ban_users,\n users.maybe_feature_flags\nFROM user_sessions\nINNER JOIN users\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"query": "\nSELECT\n users.token as `user_token: UserToken`,\n users.username,\n users.display_name,\n users.email_address,\n users.user_role_slug,\n users.is_banned,\n user_roles.can_ban_users,\n api_keys.token as `api_key_token: ApiKeyToken`\nFROM api_keys\nINNER JOIN users\n ON users.token = api_keys.owner_user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE api_keys.api_key = ?\n AND api_keys.maybe_deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -75,12 +75,12 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_feature_flags",
|
||||
"name": "api_key_token: ApiKeyToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -95,8 +95,8 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "cf2794c87868e77dfaa45bdbe630ae276dbb8c2d041442ba465af0b7a6d61af6"
|
||||
}
|
||||
"hash": "9188e235caa439d50628333c2f4722b9f1a8f575c192d4d36a2039709c412ac0"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE generic_inference_jobs\nSET\n status = ?,\n frontend_failure_category = ?,\n failure_reason = ?,\n internal_debugging_failure_reason = ?,\n success_execution_millis = ?,\n success_inference_execution_millis = ?\nWHERE token = ?\nAND status NOT IN ('complete_success', 'complete_failure')\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "96599b4854248d84188b092db84edb50dc5402bb7771fd8ea96a96ad43e732c0"
|
||||
}
|
||||
+4
-26
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n users.token as `user_token: UserToken`,\n users.username,\n users.display_name,\n users.email_address,\n users.user_role_slug,\n users.is_banned,\n user_roles.can_ban_users,\n users.maybe_feature_flags,\n api_keys.token as `api_key_token: ApiKeyToken`\nFROM api_keys\nINNER JOIN users\n ON users.token = api_keys.owner_user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE api_keys.api_key = ?\n AND api_keys.maybe_deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"query": "\nSELECT\n users.token as `user_token: UserToken`,\n users.username,\n users.display_name,\n users.email_address,\n users.user_role_slug,\n users.is_banned,\n user_roles.can_ban_users\nFROM user_sessions\nINNER JOIN users\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -72,26 +72,6 @@
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_feature_flags",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "api_key_token: ApiKeyToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -104,10 +84,8 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "62b3e2e058331a6819acd98c634b3bf2d10aa38d43b49ab69fb8637497beec2f"
|
||||
}
|
||||
"hash": "c77126adb36f9e2f0248720eb4d8c1cd9d14719f64a02678d8021e26addd2c18"
|
||||
}
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.maybe_origin_filename,\n m.maybe_duration_millis as `maybe_duration_millis: u64`,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_source_job_token = ?\n AND m.maybe_creator_user_token = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\nORDER BY m.id ASC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token: tokens::tokens::media_files::MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "media_class: enums::by_table::media_files::media_file_class::MediaFileClass",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "media_type: enums::by_table::media_files::media_file_type::MediaFileType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "public_bucket_directory_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_file_cover_image_public_bucket_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_file_cover_image_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_file_cover_image_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_origin_filename",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "maybe_duration_millis: u64",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "cd04f4f7b48ae9da53b6a210111f8e3edff94dee30263e22b45d5e29dbe9245a"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"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 maybe_batch_token = ?,\n maybe_source_job_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_generation_provider = ?,\n\n platform_type = ?,\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 ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 42
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d88098343686ded52a71fc45a1b96d28ea3003975d6e7d20eee2022aa31d8189"
|
||||
}
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n status as `status: enums::common::job_status_plus::JobStatusPlus`,\n job_type as `job_type: enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType`,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n creator_ip_address,\n maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`,\n on_success_result_entity_token as `maybe_result_media_file_token: tokens::tokens::media_files::MediaFileToken`\nFROM generic_inference_jobs\nWHERE token = ?\nAND job_type IN ('artcraft_minimax_h3_turbo', 'artcraft_minimax_h3_ultra')\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": "status: enums::common::job_status_plus::JobStatusPlus",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
|
||||
"char_set": 224,
|
||||
"max_size": 76
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "job_type: enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"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": 5,
|
||||
"name": "creator_ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 160
|
||||
}
|
||||
},
|
||||
{
|
||||
"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_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_result_media_file_token: tokens::tokens::media_files::MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f1d41ad17cb01f5aef0ce2eea042ccb3e5e649365a0befaf0f46492b75523442"
|
||||
}
|
||||
+12
@@ -0,0 +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 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_generation_provider = ?,\n\n platform_type = ?,\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 ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 41
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f5bc4a956a20f6c0998585092a0922301d53c95ae8ea9cb808ae2dbcf8d0bb32"
|
||||
}
|
||||
Generated
+27
-27
@@ -757,9 +757,9 @@ dependencies = [
|
||||
"fal_client",
|
||||
"gmicloud_client",
|
||||
"grok_api_client",
|
||||
"kinovi_web_client",
|
||||
"log",
|
||||
"reqwest 0.12.23",
|
||||
"seedance2pro_client",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -6359,29 +6359,6 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kinovi_web_client"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"cloudflare_errors",
|
||||
"env",
|
||||
"env_logger 0.11.9",
|
||||
"errors",
|
||||
"log",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"test_data",
|
||||
"test_utils",
|
||||
"tokio",
|
||||
"url",
|
||||
"workspace-build-acceleration",
|
||||
"wreq",
|
||||
"wreq-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kubectl-pod-cleanup"
|
||||
version = "0.0.1"
|
||||
@@ -10542,7 +10519,6 @@ dependencies = [
|
||||
"errors",
|
||||
"hashing",
|
||||
"jobs_common",
|
||||
"kinovi_web_client",
|
||||
"log",
|
||||
"mysql_queries",
|
||||
"pager",
|
||||
@@ -10550,6 +10526,7 @@ dependencies = [
|
||||
"reqwest 0.12.23",
|
||||
"rootly_client",
|
||||
"rootly_config",
|
||||
"seedance2pro_client",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -10561,6 +10538,29 @@ dependencies = [
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "seedance2pro_client"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"cloudflare_errors",
|
||||
"env",
|
||||
"env_logger 0.11.9",
|
||||
"errors",
|
||||
"log",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"test_data",
|
||||
"test_utils",
|
||||
"tokio",
|
||||
"url",
|
||||
"workspace-build-acceleration",
|
||||
"wreq",
|
||||
"wreq-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.24.0"
|
||||
@@ -11632,7 +11632,6 @@ dependencies = [
|
||||
"images",
|
||||
"infer 0.16.0",
|
||||
"jwt_signer",
|
||||
"kinovi_web_client",
|
||||
"language-tags",
|
||||
"lexical-sort",
|
||||
"limitation",
|
||||
@@ -11664,6 +11663,7 @@ dependencies = [
|
||||
"rootly_config",
|
||||
"rust-s3",
|
||||
"rustls 0.23.36",
|
||||
"seedance2pro_client",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -11867,8 +11867,8 @@ dependencies = [
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
"kinovi_web_client",
|
||||
"log",
|
||||
"seedance2pro_client",
|
||||
"serde_json",
|
||||
"tokens",
|
||||
"tokio",
|
||||
|
||||
+2
-2
@@ -27,7 +27,7 @@ members = [
|
||||
"crates/api_clients/openart_client",
|
||||
"crates/api_clients/rootly_client",
|
||||
"crates/api_clients/runwayml_client",
|
||||
"crates/api_clients/kinovi_web_client",
|
||||
"crates/api_clients/seedance2pro_client",
|
||||
"crates/api_clients/thumbnail_generator",
|
||||
"crates/api_clients/worldlabs_api_client",
|
||||
"crates/api_clients/worldlabs_consumer_client",
|
||||
@@ -285,7 +285,7 @@ mysql_queries = { path = "crates/schema/database/mysql_queries" }
|
||||
pager = { path = "crates/lib/pager" }
|
||||
openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
|
||||
primitives = { path = "crates/lib/data/primitives" }
|
||||
kinovi_web_client = { path = "crates/api_clients/kinovi_web_client" }
|
||||
seedance2pro_client = { path = "crates/api_clients/seedance2pro_client" }
|
||||
server_environment = { path = "crates/lib/server_environment" }
|
||||
sha1_hash = { path = "crates/lib/sha1_hash" }
|
||||
shared_service_components = { path = "crates/lib/shared_service_components" }
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files
|
||||
DROP INDEX index_maybe_source_job_token,
|
||||
DROP COLUMN maybe_source_job_token;
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- The inference job ('jinf_...') whose completion produced this media file.
|
||||
-- Only set for files written as job outputs; uploads and other origins stay NULL.
|
||||
ALTER TABLE media_files
|
||||
ADD COLUMN maybe_source_job_token VARCHAR(32) DEFAULT NULL;
|
||||
|
||||
-- For "outputs of this job"-style lookups.
|
||||
-- The column is all-NULL at creation time, so the index build is cheap.
|
||||
ALTER TABLE media_files
|
||||
ADD INDEX index_maybe_source_job_token (maybe_source_job_token);
|
||||
@@ -102,10 +102,6 @@ CREATE TABLE media_files (
|
||||
-- If the media file is generated as part of a batch, this designates the batch.
|
||||
maybe_batch_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- The inference job ('jinf_...') whose completion produced this media file.
|
||||
-- Only set for files written as job outputs; uploads and other origins stay NULL.
|
||||
maybe_source_job_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== METADATA ==========
|
||||
|
||||
-- If the file is a user upload (not including studio renders).
|
||||
@@ -331,7 +327,6 @@ CREATE TABLE media_files (
|
||||
KEY fk_maybe_origin_model_token (maybe_origin_model_token),
|
||||
KEY fk_maybe_origin_model_type_and_token (maybe_origin_model_type, maybe_origin_model_token),
|
||||
KEY index_maybe_batch_token (maybe_batch_token),
|
||||
KEY index_maybe_source_job_token (maybe_source_job_token),
|
||||
KEY index_is_user_upload (is_user_upload),
|
||||
KEY index_is_intermediate_system_file (is_intermediate_system_file),
|
||||
KEY index_maybe_scene_source_media_file_token (maybe_scene_source_media_file_token),
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
# Local Dev Stack — Requirements & Bootstrap
|
||||
|
||||
How to run `storyteller-web` on your own machine against a local MySQL and
|
||||
Redis — no shared dev database, no Docker, no admin rights. This is for **work
|
||||
on the backend itself**.
|
||||
|
||||
> **Working on the frontend?** You probably want
|
||||
> `frontend/apps/fake-storyteller-web` instead — an in-memory stand-in for this
|
||||
> API that needs no database, no build, and no setup. Everything below is
|
||||
> unnecessary for webapp work. Come back here when you need the real server.
|
||||
|
||||
These scripts install and supervise infrastructure. They change no application
|
||||
behaviour, and nothing in them is compiled into any binary: the backend they
|
||||
start is the same one that ships. Integrations they cannot provide locally
|
||||
(object storage, generation providers, email) get placeholder credentials, so
|
||||
the endpoints that use them fail when exercised. Swap in a real dev credential
|
||||
for whichever one you're actually working on.
|
||||
|
||||
The automated path on **Windows** (primary platform, native — no WSL, no
|
||||
Docker, no admin rights for the stack itself):
|
||||
|
||||
```powershell
|
||||
.\script\bootstrap\windows\bootstrap_dev_stack.ps1 # one-time setup, idempotent, re-run anytime
|
||||
.\script\bootstrap\windows\run_backend_dev.ps1 # terminal 1: API on http://localhost:12345
|
||||
.\script\bootstrap\windows\seed_demo_user.ps1 # terminal 2: create demo login (localdev1 / localdev1pass)
|
||||
$env:VITE_DEV_API_HOST = "http://localhost:12345" # terminal 2: aim the webapp at your backend
|
||||
cd frontend; npx nx dev artcraft-webapp # webapp on http://localhost:4201
|
||||
.\script\bootstrap\windows\dev_stack_doctor.ps1 # health check, any time
|
||||
.\script\bootstrap\windows\stop_dev_services.ps1 # stop MySQL/Redis when done
|
||||
```
|
||||
|
||||
The equivalent on **Linux** (Ubuntu 22.04+ — native, WSL2, or CI):
|
||||
|
||||
```bash
|
||||
./script/bootstrap/bootstrap_dev_stack.sh # one-time setup, idempotent, re-run anytime
|
||||
./script/bootstrap/run_backend_dev.sh # terminal 1: API on http://localhost:12345
|
||||
./script/bootstrap/seed_demo_user.sh # terminal 2: create demo login (localdev1 / localdev1pass)
|
||||
cd frontend && VITE_DEV_API_HOST=http://localhost:12345 npx nx dev artcraft-webapp # terminal 2: webapp on :4201
|
||||
./script/bootstrap/dev_stack_doctor.sh # health check, any time
|
||||
```
|
||||
|
||||
This document records the underlying requirements so the scripts aren't a
|
||||
black box, and corrects some stale claims in older docs.
|
||||
|
||||
## Supported platforms
|
||||
|
||||
| Platform | Status |
|
||||
|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Windows 10/11 | Primary. `script/bootstrap/windows/*.ps1` run a **portable stack**: MySQL 8.4 (official zip) and Redis live under the gitignored `.devstack/` and run as plain user processes — no admin, no services, no WSL, no Docker. Build tools (cmake, perl, nasm, llvm) are offered via winget |
|
||||
| Ubuntu 22.04+ | Supported by `script/bootstrap/*.sh` (native, WSL2, or CI runner/container); services via apt |
|
||||
| macOS | Not covered by the scripts; follow `_docs/dev_setup_server.md` manually |
|
||||
|
||||
### Windows specifics
|
||||
|
||||
- Everything stateful lives in `.devstack/` (gitignored): `mysql/` (extracted
|
||||
zip), `mysql-data/`, `redis/`, `logs/`, `pids/`, `downloads/` (cached zips),
|
||||
`my.ini`. Delete the directory to fully reset; re-run bootstrap to rebuild.
|
||||
- Pinned services: MySQL **8.4.11** from `cdn.mysql.com` (the 8.x series is
|
||||
required; only some patch versions exist on the CDN) and Redis **5.0.14.1**
|
||||
(tporadowski build — old but sufficient for the backend's keepalive/TTL/
|
||||
rate-limit usage; override `REDIS_ZIP_URL` to use Memurai/Garnet instead).
|
||||
- The portable MySQL root user has **no password** (`--initialize-insecure`) —
|
||||
local dev only, bound to 127.0.0.1.
|
||||
- diesel-cli is compiled against the portable MySQL's own client library
|
||||
(`MYSQLCLIENT_LIB_DIR`/`MYSQLCLIENT_VERSION`); `libmysql.dll` must be on
|
||||
`PATH` to *run* it — the scripts handle both.
|
||||
- Compiling the backend needs cmake, Strawberry Perl, NASM, and LLVM/libclang
|
||||
(the `wreq` HTTP client builds BoringSSL, and `aws-lc-sys`/`ring` need
|
||||
NASM/clang on MSVC). The bootstrap checks and offers winget installs.
|
||||
- The generated secrets file additionally sets `TEMP_DIR` (the upload temp-dir
|
||||
helper defaults to `/tmp`, which doesn't exist on Windows).
|
||||
|
||||
## Requirements matrix
|
||||
|
||||
| Requirement | Version / detail | Needed for |
|
||||
|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------|
|
||||
| MySQL server | 8.x series (9.x unsupported by tooling) | Backend boot (eager connect) + migrations |
|
||||
| Redis server | any recent | Backend boot (r2d2 pool connects eagerly) |
|
||||
| Rust (stable) | no pinned MSRV; Docker builds use 1.93.0 | Building `storyteller-web`, diesel_cli |
|
||||
| diesel_cli | `cargo install diesel_cli --no-default-features --features mysql,sqlite` | Running the 281 migrations |
|
||||
| Node.js | 20+ (enforced by `script/common/frontend_preflight.sh`) | Frontend (Nx 21 / Vite 6) |
|
||||
| npm | ships with Node; **pnpm must not be used** | Frontend install (`frontend/package-lock.json`) |
|
||||
| System libs (apt) | build-essential, cmake, libssl-dev, libclang-dev, libfontconfig1-dev, fontconfig, perl, pkg-config, ffmpeg, libmysqlclient-dev, libsqlite3-dev, curl, jq | Compiling the workspace + diesel_cli; seeding scripts |
|
||||
| Elasticsearch | optional (7.x) | Only 4 search endpoints; server boots without it |
|
||||
| Docker | **not required** — no docker-compose exists | — |
|
||||
|
||||
Database identity (used consistently by docs, `.env`, and defaults):
|
||||
database `storyteller`, user `storyteller`, password `password`, host
|
||||
`localhost`.
|
||||
|
||||
## Backend: what `storyteller-web` needs to boot
|
||||
|
||||
Binary: `storyteller-web` in `crates/service/web/storyteller_web`
|
||||
(binaries: `storyteller-web`, `docs` — Swagger UI on :8989, `docs-cli` — emits
|
||||
`api.json`). Binds `0.0.0.0:12345` (`BIND_ADDRESS`), plain HTTP.
|
||||
**Run it from the repo root** — the config search path and `includes/` paths
|
||||
are cwd-relative.
|
||||
|
||||
### Config loading (corrected facts)
|
||||
|
||||
- Mode is selected by the `SERVER_ENVIRONMENT` **process env var only**
|
||||
(default: Development). Setting it inside an env file does nothing — it is
|
||||
read before any file loads.
|
||||
- In Development mode, files load from
|
||||
`crates/service/web/storyteller_web/config/` in this order:
|
||||
`storyteller-web.common.env` → `storyteller-web.development.env` →
|
||||
`storyteller-web.development-secrets.env` (gitignored; the bootstrap
|
||||
generates it).
|
||||
- **Precedence is first-value-wins** (dotenv never overrides an existing key):
|
||||
real process env > common.env > development.env > development-secrets.env.
|
||||
The secrets file therefore cannot override anything already set in
|
||||
`development.env`.
|
||||
- The repo-root `.env` is **NOT loaded by the server** (it passes
|
||||
`ignore_legacy_dot_env_file: true`). Root `.env`'s `DATABASE_URL` feeds
|
||||
diesel-cli/sqlx-cli only; the server reads **`MYSQL_URL`** (default
|
||||
`mysql://storyteller:password@localhost/storyteller`).
|
||||
|
||||
### Hard requirements at boot
|
||||
|
||||
| Requirement | Behavior if missing |
|
||||
|-------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| MySQL reachable via `MYSQL_URL` | Boot fails (sqlx pool connects eagerly; the DB must exist and be migrated — health-check and ban-poller threads query immediately) |
|
||||
| Redis reachable via `REDIS_0_URL` (default `redis://localhost/0`) | Boot fails (r2d2 pool connects eagerly) |
|
||||
| ~30 `get_env_string_required` vars present | Boot fails on the first absent var |
|
||||
|
||||
Of the required-present vars, 19 are supplied by **no** checked-in file and
|
||||
live in the generated secrets file: `ACCESS_KEY`, `SECRET_KEY`, `REGION_NAME`,
|
||||
`W2L_PRIVATE_DOWNLOAD_BUCKET_NAME`, `W2L_PUBLIC_DOWNLOAD_BUCKET_NAME`,
|
||||
`RESEND_API_KEY`, `FAL_API_KEY`, `GMICLOUD_API_KEY`, `GROK_API_KEY`,
|
||||
`BEEBLE_API_KEY`, `OPENAI_API_KEY`, `WORLDLABS_API_KEY`,
|
||||
`SEEDANCE2PRO_COOKIES`, `SEEDANCE2PRO_WHITELIST_COOKIES`,
|
||||
`SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES`, `STRIPE_FAKEYOU_ACCOUNT_ID`,
|
||||
`STRIPE_ARTCRAFT_ACCOUNT_ID`, `STRIPE_ARTCRAFT_SECRET_KEY`,
|
||||
`STRIPE_ARTCRAFT_SECRET_WEBHOOK_KEY`.
|
||||
|
||||
**Placeholder values are safe at boot**: none of these services is contacted
|
||||
at boot (clients are constructed offline; Elasticsearch, S3/R2, Stripe,
|
||||
Resend, and Google-cert fetches are all lazy). But two earlier claims here
|
||||
were wrong and are corrected:
|
||||
|
||||
- **The omni_gen endpoints call providers synchronously in the HTTP handler**
|
||||
(`/v1/omni_gen/generate/{image,video}` — the path the webapp uses). With
|
||||
placeholder keys the provider call fails and the submit returns HTTP 500
|
||||
*after* billing the wallet (the image pipeline has no refund; video refunds
|
||||
only Kinovi). Only the legacy polling providers (GmiCloud, Grok, Kinovi
|
||||
orders, WorldLabs) are worker-driven.
|
||||
- **Uploads fail hard with placeholder S3 creds** — every media upload endpoint
|
||||
uploads to the bucket *before* inserting the DB row, and `rust-s3` is built
|
||||
with `fail-on-err`.
|
||||
|
||||
### What does and doesn't work locally
|
||||
|
||||
| Area | Works with placeholder credentials? |
|
||||
|----------------------------------------------|-------------------------------------|
|
||||
| Login, sessions, account, profile | Yes |
|
||||
| Wallets, credits, billing reads | Yes |
|
||||
| Folders, tags, prompts, characters, projects | Yes |
|
||||
| Media listing (rows) | Yes — but the images 404, see below |
|
||||
| Media uploads | No — needs real R2/S3 credentials |
|
||||
| Image/video/mesh/splat generation | No — needs a real provider key |
|
||||
| Transactional email | No — needs a real Resend key |
|
||||
| Elasticsearch-backed search | No — needs a local ES + reindex |
|
||||
|
||||
The rows-but-no-images case is the one that surprises people: `media_files`
|
||||
rows carry a bucket path, and the URL the API builds from it points at the
|
||||
shared R2 bucket, which does not have your local objects. Put real dev R2
|
||||
credentials in the secrets file if you need media to render, or use
|
||||
`fake-storyteller-web`, which serves its own bytes.
|
||||
|
||||
### Auth on localhost
|
||||
|
||||
- `POST /v1/login` takes `{"username_or_email": "...", "password": "..."}`;
|
||||
success returns `{"success": true, "signed_session": "<jwt>"}` **and** a
|
||||
`session` cookie.
|
||||
- With the checked-in dev config (`COOKIE_DOMAIN=.jungle.horse`), the session
|
||||
cookie is issued non-Secure and host-only (the Domain attribute is never
|
||||
set), so plain-HTTP localhost login works as-is. The frontend additionally
|
||||
stores `signed_session` in localStorage and sends it as a `session` header.
|
||||
- Dev-mode CORS allows any `localhost`/`127.0.0.1` origin on any port, with
|
||||
credentials.
|
||||
- Health endpoint for scripts/CI: `GET /_status`.
|
||||
|
||||
### Build
|
||||
|
||||
- Compile with `SQLX_OFFLINE=true` (uses the checked-in `.sqlx/` cache; 483
|
||||
queries). Without it, the sqlx macros open a **live DB connection at compile
|
||||
time** using `DATABASE_URL` from the root `.env`. `SQLX_OFFLINE` has no
|
||||
runtime effect — ignore older notes saying not to set it when running.
|
||||
- First build takes several minutes; the crate has ~370 pre-existing warnings.
|
||||
|
||||
## Database: migrations and seed data
|
||||
|
||||
- 281 Diesel migrations in `_database/sql/migrations/` (`diesel.toml` at repo
|
||||
root points there). Run from the repo root: `diesel migration run`.
|
||||
`"Encountered unknown type for Mysql: enum"` warnings are harmless.
|
||||
`migrations_squashed/` is documentation only — not executable.
|
||||
- Seed data: `_database/sql/seed/sql/system_roles.sql` (roles `user`, `mod`,
|
||||
`admin`) and `user_badges.sql`. **The `user` role is mandatory** — account
|
||||
creation hardcodes `user_role_slug='user'`. The legacy wrapper
|
||||
`bootstrap_inserts_roles_etc.sh` must run with cwd `_database/sql/` and is
|
||||
not idempotent; the bootstrap script applies the same SQL files with
|
||||
idempotency guards instead.
|
||||
- Demo user: created through **`POST /v1/create_account`** (no captcha, no
|
||||
invite code, no email-verification gate; password just needs 6+ chars). This
|
||||
exercises the real signup path instead of hand-rolled SQL. Gotcha: usernames
|
||||
like `demo`, `admin`, `test`, `dev`, `artcraft` are reserved
|
||||
(`includes/binary_includes/usernames/reserved_usernames.txt`) — the default
|
||||
demo user is `localdev1`. The Rust seeder
|
||||
(`cargo run --bin dev-database-seed`) exists but hard-requires a
|
||||
`.env-secrets` file and also seeds model weights, so the HTTP path is
|
||||
preferred.
|
||||
- Demo credits: signup creates **no wallet**, and the webapp gates the
|
||||
generate button on wallet balance — so `seed_demo_user` also seeds an
|
||||
`artcraft`-namespace wallet with `DEMO_CREDITS` (default 100,000) banked
|
||||
credits, plus matching ledger rows. Idempotent: re-runs top the balance
|
||||
back up to `DEMO_CREDITS` but never lower it. (Raw SQL, because the only
|
||||
real credit-granting paths are Stripe webhooks and moderator tooling.)
|
||||
- Demo gallery: **not seeded**. Media rows without reachable bytes render as
|
||||
broken tiles, so seeding them locally would be worse than an empty library.
|
||||
Use `fake-storyteller-web` for a populated one.
|
||||
|
||||
## Frontend: `artcraft-webapp`
|
||||
|
||||
- Nx workspace at repo-root `frontend/` (npm workspaces; `npm install` from
|
||||
`frontend/`). The FontAwesome Pro registry token is committed in
|
||||
`frontend/.npmrc` — no manual auth step.
|
||||
- Serve: `npx nx dev artcraft-webapp` → `http://localhost:4201`. No lib
|
||||
pre-build is needed in dev (libs resolve to TS source via the
|
||||
`"development"` export condition); production `nx build` does need lib
|
||||
builds.
|
||||
- **By default the dev webapp targets production.** `src/main.tsx` points the
|
||||
API host at the Vite dev origin, and Vite proxies `/v1` to
|
||||
`https://api.storyteller.ai`. The adjacent `setDevelopment()` call — which
|
||||
would point it at `localhost:12345` — is commented out. To aim the webapp at
|
||||
your local backend, set `VITE_DEV_API_HOST=http://localhost:12345` before
|
||||
`nx dev` (that override ships with the `fake-storyteller-web` change; until
|
||||
it lands, uncomment `setDevelopment()` locally instead).
|
||||
- Optional: `VITE_GOOGLE_CLIENT_ID` in `frontend/apps/artcraft-webapp/.env`
|
||||
enables the Google SSO button; email/password login needs nothing.
|
||||
- Other dev ports if you run more apps: website 4200, dashboard 4202 (separate
|
||||
pnpm app, excluded from Nx), video-info-website 4250, genhub 4300, Tauri
|
||||
frontend 5173.
|
||||
|
||||
## What still touches production (known, deliberate seams)
|
||||
|
||||
The bootstrap changes no runtime behaviour, so these remain open. Each is
|
||||
closed by supplying a real credential, not by a code path:
|
||||
|
||||
| Seam | Today |
|
||||
|---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Media/CDN URLs in API responses | The compiled-in `cdn_link.rs` constants point at the public `pub-….r2.dev` bucket, so a locally-created row's media URL resolves to an object that isn't there |
|
||||
| Media uploads | 500 with placeholder credentials; supply real dev R2/S3 keys to exercise them |
|
||||
| Generation | 500 with placeholder keys, *after* the wallet is billed; supply a real key for the provider under test |
|
||||
| Vite `/v1` proxy in webapp | Targets `https://api.storyteller.ai`, so a dev webapp with no host override reads and writes production |
|
||||
| Elasticsearch-backed search | `search_featured` / `search_session` media files, `weights/search`, legacy `tts/search` fail without a local ES + `elasticsearch-cli` reindex (optional) |
|
||||
|
||||
## CI usage
|
||||
|
||||
The same bootstrap works on a fresh Ubuntu runner/container:
|
||||
|
||||
```bash
|
||||
export MYSQL_ROOT_PASSWORD=root # GitHub Actions' preinstalled MySQL uses password auth
|
||||
./script/bootstrap/bootstrap_dev_stack.sh --yes
|
||||
./script/bootstrap/run_backend_dev.sh &
|
||||
./script/bootstrap/seed_demo_user.sh # waits for /_status
|
||||
./script/bootstrap/dev_stack_doctor.sh
|
||||
```
|
||||
|
||||
`--yes` answers all prompts; `--skip-frontend` / `--skip-rust-build` /
|
||||
`--skip-packages` slice the work for focused jobs.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
- `./script/bootstrap/dev_stack_doctor.sh` exits 0 with no FAILs.
|
||||
- `POST /v1/login` with the demo user returns `success: true` plus a
|
||||
`signed_session` (done automatically by `seed_demo_user.sh`).
|
||||
- `GET /_status` returns 200, and MySQL has the full migration set applied.
|
||||
- With `VITE_DEV_API_HOST` pointed at `localhost:12345`, the webapp logs in as
|
||||
`localdev1` and shows its credits in the top bar. The library is empty and
|
||||
generation 500s — both expected, see the table above.
|
||||
|
||||
## Corrections to older docs
|
||||
|
||||
- `crates/service/web/storyteller_web/CLAUDE.md`: the repo-root `.env` is
|
||||
*not* auto-loaded by the server, and `SQLX_OFFLINE=true` is fine (and
|
||||
recommended) when running — it only affects compile-time query checking.
|
||||
- `_docs/dev_setup_server.md`: `./script/dev_mysql_connect.sh` doesn't exist —
|
||||
it's `./script/dev_database_connect.sh`.
|
||||
- `diesel.toml`'s comment about needing an unreleased diesel CLI is stale —
|
||||
any current `diesel_cli` works.
|
||||
- `TESTING_INFRA_HANDOFF.md` (untracked): the server env var is `MYSQL_URL`,
|
||||
not `DATABASE_URL`; migration count is 281; crate path is
|
||||
`crates/service/web/storyteller_web`.
|
||||
+2
-2
@@ -51,9 +51,9 @@ helpers go in the parent `mod tests` so sub-modules can `use super::*`.
|
||||
|
||||
- `storyteller_web` — main HTTP API monolith (actix-web)
|
||||
- `artcraft` (desktop) — Tauri desktop app
|
||||
- `artcraft_router` — provider routing for image/video generation (Artcraft, Fal, KinoviWeb, Muapi)
|
||||
- `artcraft_router` — provider routing for image/video generation (Artcraft, Fal, Seedance2Pro, Muapi)
|
||||
- `artcraft_api_defs` — shared HTTP API type definitions
|
||||
- `kinovi_web_client` — HTTP client for the Kinovi/KinoviWeb video generation service
|
||||
- `seedance2pro_client` — HTTP client for the Kinovi/Seedance2Pro video generation service
|
||||
- `enums` — database-backed enums stored as VARCHAR fields
|
||||
- `tokens` — primary key identifiers with Stripe-like prefixes (e.g. `user_`, `mf_`)
|
||||
- `mysql_queries` — SQLx MySQL queries for the backend
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
|
||||
|
||||
/// Request body for `POST /v1/internal/minimax_jobs/job/{job_token}/failure`.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MarkMinimaxJobFailureRequest {
|
||||
/// Optional failure class the frontend can localize and present.
|
||||
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
|
||||
|
||||
/// Optional failure reason we can show the user (512 characters max).
|
||||
pub maybe_user_failure_reason: Option<String>,
|
||||
|
||||
/// Optional internal-only stack trace or error (512 characters max).
|
||||
pub maybe_internal_debugging_failure_reason: Option<String>,
|
||||
|
||||
/// Optional total wall-clock runtime of the failed attempt, in milliseconds.
|
||||
pub execution_duration_millis: Option<u64>,
|
||||
|
||||
/// Optional inference-only runtime of the failed attempt, in milliseconds.
|
||||
pub inference_duration_millis: Option<u64>,
|
||||
}
|
||||
|
||||
/// Response body for `POST /v1/internal/minimax_jobs/job/{job_token}/failure`.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MarkMinimaxJobFailureResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
// NB: The success request is form-multipart (video bytes + optional
|
||||
// `duration_millis` / `width` / `height` metadata fields), so its form struct
|
||||
// lives with the actix handler; only the response shape is declared here.
|
||||
|
||||
/// Response body for `POST /v1/internal/minimax_jobs/job/{job_token}/success`.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MarkMinimaxJobSuccessResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// The media file created from the uploaded video.
|
||||
pub media_file_token: MediaFileToken,
|
||||
|
||||
/// Video duration stored on the media file record, in milliseconds
|
||||
/// (worker-supplied, or probed from the file when not supplied).
|
||||
pub maybe_duration_millis: Option<u64>,
|
||||
|
||||
/// Frame width stored on the media file record, in pixels
|
||||
/// (worker-supplied, or probed from the file when not supplied).
|
||||
pub maybe_width: Option<u32>,
|
||||
|
||||
/// Frame height stored on the media file record, in pixels
|
||||
/// (worker-supplied, or probed from the file when not supplied).
|
||||
pub maybe_height: Option<u32>,
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// The first-party Minimax models a worker can request jobs for.
|
||||
///
|
||||
/// This is deliberately its own enum (not `CommonVideoModel`) so the internal
|
||||
/// worker API surface stays small and stable.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
|
||||
pub enum MinimaxWorkerModel {
|
||||
#[serde(rename = "minimax_h3_turbo")]
|
||||
MinimaxH3Turbo,
|
||||
|
||||
#[serde(rename = "minimax_h3_ultra")]
|
||||
MinimaxH3Ultra,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn serde_round_trip() {
|
||||
let cases = [
|
||||
(MinimaxWorkerModel::MinimaxH3Turbo, "\"minimax_h3_turbo\""),
|
||||
(MinimaxWorkerModel::MinimaxH3Ultra, "\"minimax_h3_ultra\""),
|
||||
];
|
||||
for (variant, json) in cases {
|
||||
assert_eq!(serde_json::to_string(&variant).unwrap(), json);
|
||||
let parsed: MinimaxWorkerModel = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(parsed, variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod mark_minimax_job_failure;
|
||||
pub mod mark_minimax_job_success;
|
||||
pub mod minimax_worker_model;
|
||||
pub mod obtain_minimax_job;
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_bitrate::CommonBitrate;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
|
||||
use crate::common::responses::media_links::MediaLinks;
|
||||
use crate::internal::minimax_jobs::minimax_worker_model::MinimaxWorkerModel;
|
||||
|
||||
/// Request body for `POST /v1/internal/minimax_jobs/obtain_job`.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ObtainMinimaxJobRequest {
|
||||
/// Which model the worker wants a job for.
|
||||
pub model: MinimaxWorkerModel,
|
||||
|
||||
/// The worker's hostname (linux hostname, k8s pod name), if reported.
|
||||
pub worker_hostname: Option<String>,
|
||||
|
||||
/// The cluster the worker runs in (e.g. "runpod", "lambdalabs"), if reported.
|
||||
pub cluster_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Response body for `POST /v1/internal/minimax_jobs/obtain_job`.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ObtainMinimaxJobResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// The obtained job, or `None` when no pending job is available.
|
||||
pub maybe_job: Option<ObtainedMinimaxJob>,
|
||||
}
|
||||
|
||||
/// A job the worker now holds a lock on (status was moved to `started`).
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ObtainedMinimaxJob {
|
||||
pub job_token: InferenceJobToken,
|
||||
|
||||
/// Echo of the requested model.
|
||||
pub model: MinimaxWorkerModel,
|
||||
|
||||
/// The prompt and inputs the worker needs to run inference. Only `None`
|
||||
/// when the job was somehow enqueued without a prompt record (or the
|
||||
/// prompt lookup failed) — workers should fail such jobs via the failure
|
||||
/// endpoint.
|
||||
pub maybe_prompt: Option<MinimaxJobPromptDetails>,
|
||||
}
|
||||
|
||||
/// The prompt (text + generation settings) and its attached media references.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MinimaxJobPromptDetails {
|
||||
pub prompt_token: PromptToken,
|
||||
|
||||
pub maybe_positive_prompt: Option<String>,
|
||||
pub maybe_negative_prompt: Option<String>,
|
||||
|
||||
pub maybe_generation_mode: Option<CommonGenerationMode>,
|
||||
pub maybe_aspect_ratio: Option<CommonAspectRatio>,
|
||||
pub maybe_resolution: Option<CommonResolution>,
|
||||
pub maybe_bitrate: Option<CommonBitrate>,
|
||||
pub maybe_generate_audio: Option<bool>,
|
||||
pub maybe_duration_seconds: Option<u32>,
|
||||
|
||||
/// Image / video / audio references attached to the prompt, with full CDN
|
||||
/// links, in their original order.
|
||||
pub media_references: Vec<MinimaxJobMediaReference>,
|
||||
}
|
||||
|
||||
/// One media input attached to the prompt (start frame, end frame, or an
|
||||
/// image / video / audio reference), with full CDN links.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MinimaxJobMediaReference {
|
||||
pub media_file_token: MediaFileToken,
|
||||
|
||||
/// The role the media plays (start frame, end frame, image / video / audio
|
||||
/// reference, etc.).
|
||||
pub semantic_type: PromptContextSemanticType,
|
||||
|
||||
/// Full CDN links to the media file.
|
||||
pub media_links: MediaLinks,
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
//! Internal-facing API surfaces (worker fleets, etc.). These are
|
||||
//! authenticated with internal API keys, never consumer credentials.
|
||||
|
||||
pub mod minimax_jobs;
|
||||
@@ -5,7 +5,6 @@ pub mod common;
|
||||
pub mod credits;
|
||||
pub mod folders;
|
||||
pub mod generate;
|
||||
pub mod internal;
|
||||
pub mod jobs;
|
||||
pub mod media_file;
|
||||
pub mod moderation;
|
||||
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
//! Types for `GET /v1/media_files/by_job/{job_token}` — list the media files
|
||||
//! produced by an inference job.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
|
||||
use crate::common::responses::media_file_cover_image_details::MediaFileCoverImageDetails;
|
||||
use crate::common::responses::media_links::MediaLinks;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct ListMediaFilesByJobSuccessResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// The job's output files, oldest first.
|
||||
pub media_files: Vec<JobMediaFileInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct JobMediaFileInfo {
|
||||
pub token: MediaFileToken,
|
||||
|
||||
/// The coarse-grained class of media file: image, video, etc.
|
||||
pub media_class: MediaFileClass,
|
||||
|
||||
/// Type of media will dictate which fields are populated and what
|
||||
/// the frontend should display (eg. video player vs audio player).
|
||||
/// This is closer in meaning to a "mime type".
|
||||
pub media_type: MediaFileType,
|
||||
|
||||
/// If the file was generated as part of a batch, this is the token for the batch.
|
||||
pub maybe_batch_token: Option<BatchGenerationToken>,
|
||||
|
||||
/// The foreign key to the prompt used to generate the media, if applicable.
|
||||
pub maybe_prompt_token: Option<PromptToken>,
|
||||
|
||||
/// Rich CDN links to the media, including thumbnails, previews, and more.
|
||||
pub media_links: MediaLinks,
|
||||
|
||||
/// Information about the cover image. Many media files do not require a cover image,
|
||||
/// e.g. image files, video files with thumbnails, audio files, etc.
|
||||
/// 3D files require them.
|
||||
pub cover_image: MediaFileCoverImageDetails,
|
||||
|
||||
/// The original filename for uploaded files, if they were provided.
|
||||
/// In the future we'll provide our own internal optional filenames.
|
||||
pub maybe_original_filename: Option<String>,
|
||||
|
||||
/// Duration for audio and video files, if available.
|
||||
/// Measured in milliseconds.
|
||||
pub maybe_duration_millis: Option<u64>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod list_media_files_by_job;
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod delete_media_file;
|
||||
pub mod job;
|
||||
pub mod list;
|
||||
pub mod list_batch_generated_media_files;
|
||||
pub mod list_session_project_media_files;
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ pub struct OmniGenVideoGenerateResponse {
|
||||
|
||||
/// All job tokens created by this request (including the primary).
|
||||
/// For single-job requests this will contain just one element matching
|
||||
/// `inference_job_token`. For batch requests (eg. KinoviWeb with
|
||||
/// `inference_job_token`. For batch requests (eg. Seedance2Pro with
|
||||
/// multiple order IDs) this will contain all of them.
|
||||
pub all_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ artcraft_client.workspace = true
|
||||
fal_client.workspace = true
|
||||
gmicloud_client.workspace = true
|
||||
grok_api_client.workspace = true
|
||||
kinovi_web_client.workspace = true
|
||||
seedance2pro_client.workspace = true
|
||||
tokens.workspace = true
|
||||
url_utils.workspace = true
|
||||
worldlabs_api_client.workspace = true
|
||||
|
||||
@@ -8,6 +8,6 @@ pub enum RouterProvider {
|
||||
Fal,
|
||||
GmiCloud,
|
||||
GrokApi,
|
||||
KinoviWeb,
|
||||
Seedance2Pro,
|
||||
WorldLabs,
|
||||
}
|
||||
|
||||
@@ -14,12 +14,6 @@ pub enum RouterVideoModel {
|
||||
#[serde(rename = "grok_imagine_video_1p5")]
|
||||
GrokImagineVideo1p5,
|
||||
|
||||
#[serde(rename = "flux_3")]
|
||||
Flux3,
|
||||
|
||||
#[serde(rename = "flux_3_draft")]
|
||||
Flux3Draft,
|
||||
|
||||
#[serde(rename = "kling_1p6_pro")]
|
||||
Kling16Pro,
|
||||
|
||||
@@ -41,15 +35,6 @@ pub enum RouterVideoModel {
|
||||
#[serde(rename = "kling_3p0_pro")]
|
||||
Kling3p0Pro,
|
||||
|
||||
#[serde(rename = "minimax_h3")]
|
||||
MinimaxH3,
|
||||
|
||||
#[serde(rename = "minimax_h3_turbo")]
|
||||
MinimaxH3Turbo,
|
||||
|
||||
#[serde(rename = "minimax_h3_ultra")]
|
||||
MinimaxH3Ultra,
|
||||
|
||||
#[serde(rename = "seedance_1p0_lite")]
|
||||
Seedance10Lite,
|
||||
|
||||
@@ -89,9 +74,6 @@ pub enum RouterVideoModel {
|
||||
#[serde(rename = "seedance_2p0_bpu_mini")]
|
||||
Seedance2p0BytePlusUltraMini,
|
||||
|
||||
#[serde(rename = "seedance_2p5_preview")]
|
||||
Seedance2p5Preview,
|
||||
|
||||
#[serde(rename = "happy_horse_1p0")]
|
||||
HappyHorse1p0,
|
||||
|
||||
@@ -151,11 +133,6 @@ mod tests {
|
||||
assert_serde_round_trip(RouterVideoModel::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seedance_2p5_preview_serializes() {
|
||||
assert_serde_round_trip(RouterVideoModel::Seedance2p5Preview, "seedance_2p5_preview");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn veo_3p1_lite_and_vidu_variants_serialize() {
|
||||
// NB: These strings must match `CommonVideoModel` — the two enums convert
|
||||
@@ -164,27 +141,4 @@ mod tests {
|
||||
assert_serde_round_trip(RouterVideoModel::ViduQ3, "vidu_q3");
|
||||
assert_serde_round_trip(RouterVideoModel::ViduQ3Turbo, "vidu_q3_turbo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flux_3_variants_serialize() {
|
||||
// NB: These strings must match `CommonVideoModel` — the two enums convert
|
||||
// via serde string round-trip in the omni handlers.
|
||||
assert_serde_round_trip(RouterVideoModel::Flux3, "flux_3");
|
||||
assert_serde_round_trip(RouterVideoModel::Flux3Draft, "flux_3_draft");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimax_h3_serializes() {
|
||||
// NB: This string must match `CommonVideoModel` — the two enums convert
|
||||
// via serde string round-trip in the omni handlers.
|
||||
assert_serde_round_trip(RouterVideoModel::MinimaxH3, "minimax_h3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimax_h3_turbo_and_ultra_serialize() {
|
||||
// NB: These strings must match `CommonVideoModel` — the two enums convert
|
||||
// via serde string round-trip in the omni handlers.
|
||||
assert_serde_round_trip(RouterVideoModel::MinimaxH3Turbo, "minimax_h3_turbo");
|
||||
assert_serde_round_trip(RouterVideoModel::MinimaxH3Ultra, "minimax_h3_ultra");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ pub mod router_client;
|
||||
pub mod router_fal_client;
|
||||
pub mod router_gmicloud_client;
|
||||
pub mod router_grok_api_client;
|
||||
pub mod router_kinovi_web_client;
|
||||
pub mod router_seedance2pro_client;
|
||||
pub mod router_worldlabs_client;
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
|
||||
use crate::errors::client_error::{ClientError, ClientType};
|
||||
|
||||
@@ -11,7 +11,7 @@ pub struct MultiRouterClient {
|
||||
pub(crate) fal_client: Option<RouterFalClient>,
|
||||
pub(crate) gmicloud_client: Option<RouterGmiCloudClient>,
|
||||
pub(crate) grok_api_client: Option<RouterGrokApiClient>,
|
||||
pub(crate) kinovi_web_client: Option<RouterKinoviWebClient>,
|
||||
pub(crate) seedance2pro_client: Option<RouterSeedance2ProClient>,
|
||||
pub(crate) worldlabs_client: Option<RouterWorldLabsClient>,
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ impl MultiRouterClient {
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::GrokApi))
|
||||
}
|
||||
|
||||
pub fn get_kinovi_web_client_ref(&self) -> Result<&RouterKinoviWebClient, ClientError> {
|
||||
self.kinovi_web_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::KinoviWeb))
|
||||
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ClientError> {
|
||||
self.seedance2pro_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::Seedance2Pro))
|
||||
}
|
||||
|
||||
pub fn get_worldlabs_client_ref(&self) -> Result<&RouterWorldLabsClient, ClientError> {
|
||||
|
||||
+6
-6
@@ -3,7 +3,7 @@ use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
|
||||
|
||||
pub struct MultiRouterClientBuilder {
|
||||
@@ -11,7 +11,7 @@ pub struct MultiRouterClientBuilder {
|
||||
fal_client: Option<RouterFalClient>,
|
||||
gmicloud_client: Option<RouterGmiCloudClient>,
|
||||
grok_api_client: Option<RouterGrokApiClient>,
|
||||
kinovi_web_client: Option<RouterKinoviWebClient>,
|
||||
seedance2pro_client: Option<RouterSeedance2ProClient>,
|
||||
worldlabs_client: Option<RouterWorldLabsClient>,
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ impl MultiRouterClientBuilder {
|
||||
fal_client: None,
|
||||
gmicloud_client: None,
|
||||
grok_api_client: None,
|
||||
kinovi_web_client: None,
|
||||
seedance2pro_client: None,
|
||||
worldlabs_client: None,
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,8 @@ impl MultiRouterClientBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_kinovi_web_client(mut self, client: RouterKinoviWebClient) -> Self {
|
||||
self.kinovi_web_client = Some(client);
|
||||
pub fn set_seedance2pro_client(mut self, client: RouterSeedance2ProClient) -> Self {
|
||||
self.seedance2pro_client = Some(client);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ impl MultiRouterClientBuilder {
|
||||
fal_client: self.fal_client,
|
||||
gmicloud_client: self.gmicloud_client,
|
||||
grok_api_client: self.grok_api_client,
|
||||
kinovi_web_client: self.kinovi_web_client,
|
||||
seedance2pro_client: self.seedance2pro_client,
|
||||
worldlabs_client: self.worldlabs_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
|
||||
use crate::errors::client_error::{ClientError, ClientType};
|
||||
|
||||
@@ -13,7 +13,7 @@ pub enum RouterClient {
|
||||
Fal(RouterFalClient),
|
||||
GmiCloud(RouterGmiCloudClient),
|
||||
GrokApi(RouterGrokApiClient),
|
||||
KinoviWeb(RouterKinoviWebClient),
|
||||
Seedance2Pro(RouterSeedance2ProClient),
|
||||
WorldLabs(RouterWorldLabsClient),
|
||||
}
|
||||
|
||||
@@ -50,11 +50,11 @@ impl RouterClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_kinovi_web_client_ref(&self) -> Result<&RouterKinoviWebClient, ClientError> {
|
||||
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ClientError> {
|
||||
match self {
|
||||
RouterClient::KinoviWeb(client) => Ok(client),
|
||||
RouterClient::Multi(multi) => multi.get_kinovi_web_client_ref(),
|
||||
_ => Err(ClientError::ClientNotConfigured(ClientType::KinoviWeb)),
|
||||
RouterClient::Seedance2Pro(client) => Ok(client),
|
||||
RouterClient::Multi(multi) => multi.get_seedance2pro_client_ref(),
|
||||
_ => Err(ClientError::ClientNotConfigured(ClientType::Seedance2Pro)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
|
||||
|
||||
pub struct RouterKinoviWebClient {
|
||||
pub(crate) session: KinoviWebSession,
|
||||
}
|
||||
|
||||
impl RouterKinoviWebClient {
|
||||
pub fn new(session: KinoviWebSession) -> Self {
|
||||
RouterKinoviWebClient { session }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
|
||||
pub struct RouterSeedance2ProClient {
|
||||
pub(crate) session: Seedance2ProSession,
|
||||
}
|
||||
|
||||
impl RouterSeedance2ProClient {
|
||||
pub fn new(session: Seedance2ProSession) -> Self {
|
||||
RouterSeedance2ProClient { session }
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ use crate::errors::client_error::ClientError;
|
||||
use crate::errors::download_error::DownloadError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use kinovi_web_client::error::kinovi_web_error::KinoviWebError;
|
||||
use kinovi_web_client::error::kinovi_web_specific_api_error::KinoviWebSpecificApiError;
|
||||
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
|
||||
use seedance2pro_client::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use artcraft_client::error::api_error::ApiError;
|
||||
@@ -65,7 +65,7 @@ impl From<ProviderError> for ArtcraftRouterError {
|
||||
fn from(error: ProviderError) -> Self {
|
||||
let is_billing_error = match &error {
|
||||
ProviderError::Fal(FalErrorPlus::FalBillingError(_)) => true,
|
||||
ProviderError::KinoviWeb(KinoviWebError::ApiSpecific(KinoviWebSpecificApiError::BillingError { .. })) => true,
|
||||
ProviderError::Seedance2Pro(Seedance2ProError::ApiSpecific(Seedance2ProSpecificApiError::BillingError { .. })) => true,
|
||||
ProviderError::Storyteller(StorytellerError::Api(ApiError::PaymentRequired(_))) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ pub enum ClientType {
|
||||
Fal,
|
||||
GmiCloud,
|
||||
GrokApi,
|
||||
KinoviWeb,
|
||||
Seedance2Pro,
|
||||
WorldLabs,
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ impl Display for ClientType {
|
||||
Self::Fal => write!(f, "Fal"),
|
||||
Self::GmiCloud => write!(f, "GmiCloud"),
|
||||
Self::GrokApi => write!(f, "GrokApi"),
|
||||
Self::KinoviWeb => write!(f, "KinoviWeb"),
|
||||
Self::Seedance2Pro => write!(f, "Seedance2Pro"),
|
||||
Self::WorldLabs => write!(f, "WorldLabs"),
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,8 @@ pub enum ClientError {
|
||||
/// Fal only accepts image URLs for image inputs, not media tokens.
|
||||
FalOnlySupportsUrls,
|
||||
|
||||
/// KinoviWeb only accepts URLs for media inputs, not media tokens.
|
||||
KinoviWebOnlySupportsUrls,
|
||||
/// Seedance2Pro only accepts URLs for media inputs, not media tokens.
|
||||
Seedance2ProOnlySupportsUrls,
|
||||
|
||||
/// The Fal endpoint requires a webhook URL but the caller built the client
|
||||
/// in polling/queue mode (no webhook URL). Returned by webhook-only
|
||||
@@ -91,8 +91,8 @@ impl Display for ClientError {
|
||||
Self::FalOnlySupportsUrls => {
|
||||
write!(f, "Fal only supports image URLs for image inputs, not media tokens")
|
||||
}
|
||||
Self::KinoviWebOnlySupportsUrls => {
|
||||
write!(f, "KinoviWeb only supports URLs for media inputs; resolve media tokens to URLs before calling this provider")
|
||||
Self::Seedance2ProOnlySupportsUrls => {
|
||||
write!(f, "Seedance2Pro only supports URLs for media inputs; resolve media tokens to URLs before calling this provider")
|
||||
}
|
||||
Self::WebhookUrlRequired => {
|
||||
write!(f, "This Fal endpoint only supports webhook dispatch; the caller built RouterFalClient in polling-only mode (no webhook URL)")
|
||||
|
||||
@@ -4,7 +4,7 @@ use artcraft_client::error::storyteller_error::StorytellerError;
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use gmicloud_client::error::gmicloud_error::GmiCloudError;
|
||||
use grok_api_client::error::grok_error::GrokError;
|
||||
use kinovi_web_client::error::kinovi_web_error::KinoviWebError;
|
||||
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
|
||||
use worldlabs_api_client::error::world_labs_error::WorldLabsError;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -13,7 +13,7 @@ pub enum ProviderError {
|
||||
Fal(FalErrorPlus),
|
||||
GmiCloud(GmiCloudError),
|
||||
Grok(GrokError),
|
||||
KinoviWeb(KinoviWebError),
|
||||
Seedance2Pro(Seedance2ProError),
|
||||
WorldLabs(WorldLabsError),
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ impl Display for ProviderError {
|
||||
Self::Fal(e) => write!(f, "Fal provider error: {}", e),
|
||||
Self::GmiCloud(e) => write!(f, "GmiCloud provider error: {}", e),
|
||||
Self::Grok(e) => write!(f, "Grok provider error: {}", e),
|
||||
Self::KinoviWeb(e) => write!(f, "KinoviWeb provider error: {}", e),
|
||||
Self::Seedance2Pro(e) => write!(f, "Seedance2Pro provider error: {}", e),
|
||||
Self::WorldLabs(e) => write!(f, "WorldLabs provider error: {}", e),
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,9 @@ impl From<GrokError> for ProviderError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KinoviWebError> for ProviderError {
|
||||
fn from(error: KinoviWebError) -> Self {
|
||||
Self::KinoviWeb(error)
|
||||
impl From<Seedance2ProError> for ProviderError {
|
||||
fn from(error: Seedance2ProError) -> Self {
|
||||
Self::Seedance2Pro(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -26,8 +26,8 @@ impl AudioGenerationDraftRequest {
|
||||
|
||||
pub fn get_provider(&self) -> RouterProvider {
|
||||
match self {
|
||||
Self::KinoviSunoRemix(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviSunoSample(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviSunoRemix(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSunoSample(_) => RouterProvider::Seedance2Pro,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ use std::fmt::Debug;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
@@ -19,9 +19,9 @@ pub struct AudioGenerationDraftContext<'a> {
|
||||
}
|
||||
|
||||
impl <'a> AudioGenerationDraftContext<'a> {
|
||||
pub fn get_kinovi_web_client_ref(&self) -> Result<&RouterKinoviWebClient, ArtcraftRouterError> {
|
||||
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ArtcraftRouterError> {
|
||||
let client = self.client.ok_or(ArtcraftRouterError::Client(ClientError::RouterClientNotProvided))?;
|
||||
client.get_kinovi_web_client_ref()
|
||||
client.get_seedance2pro_client_ref()
|
||||
.map_err(|err| ArtcraftRouterError::Client(err))
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -48,10 +48,10 @@ impl AudioGenerationRequest {
|
||||
Self::ArtcraftSunoSample(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftSeedAudio1p0(_) => RouterProvider::Artcraft,
|
||||
Self::FalSeedAudio1p0(_) => RouterProvider::Fal,
|
||||
Self::KinoviSunoMusic(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviSunoRemix(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviSunoSounds(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviSunoSample(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviSunoMusic(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSunoRemix(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSunoSounds(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSunoSample(_) => RouterProvider::Seedance2Pro,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,19 +100,19 @@ impl AudioGenerationRequest {
|
||||
request.send(client_ref).await
|
||||
},
|
||||
AudioGenerationRequest::KinoviSunoMusic(request) => {
|
||||
let client_ref = client.get_kinovi_web_client_ref()?;
|
||||
let client_ref = client.get_seedance2pro_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
AudioGenerationRequest::KinoviSunoRemix(request) => {
|
||||
let client_ref = client.get_kinovi_web_client_ref()?;
|
||||
let client_ref = client.get_seedance2pro_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
AudioGenerationRequest::KinoviSunoSounds(request) => {
|
||||
let client_ref = client.get_kinovi_web_client_ref()?;
|
||||
let client_ref = client.get_seedance2pro_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
AudioGenerationRequest::KinoviSunoSample(request) => {
|
||||
let client_ref = client.get_kinovi_web_client_ref()?;
|
||||
let client_ref = client.get_seedance2pro_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
}
|
||||
|
||||
+9
-9
@@ -116,10 +116,10 @@ impl GenerateAudioRequestBuilder {
|
||||
// Fal
|
||||
(RouterProvider::Fal, RouterAudioModel::SeedAudio1p0) => build_fal_seed_audio_1p0(self),
|
||||
// Kinovi
|
||||
(RouterProvider::KinoviWeb, RouterAudioModel::SunoMusic) => build_kinovi_suno_music(self),
|
||||
(RouterProvider::KinoviWeb, RouterAudioModel::SunoRemix) => build_kinovi_suno_remix(self),
|
||||
(RouterProvider::KinoviWeb, RouterAudioModel::SunoSounds) => build_kinovi_suno_sounds(self),
|
||||
(RouterProvider::KinoviWeb, RouterAudioModel::SunoSample) => build_kinovi_suno_sample(self),
|
||||
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoMusic) => build_kinovi_suno_music(self),
|
||||
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoRemix) => build_kinovi_suno_remix(self),
|
||||
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSounds) => build_kinovi_suno_sounds(self),
|
||||
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSample) => build_kinovi_suno_sample(self),
|
||||
_ => self.unsupported_provider_and_model(),
|
||||
}
|
||||
}
|
||||
@@ -202,7 +202,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn kinovi_suno_music_dispatches_to_request() {
|
||||
let result = builder(RouterProvider::KinoviWeb, RouterAudioModel::SunoMusic).build2().expect("build");
|
||||
let result = builder(RouterProvider::Seedance2Pro, RouterAudioModel::SunoMusic).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::KinoviSunoMusic(_))
|
||||
@@ -211,7 +211,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn kinovi_suno_sounds_dispatches_to_request() {
|
||||
let result = builder(RouterProvider::KinoviWeb, RouterAudioModel::SunoSounds).build2().expect("build");
|
||||
let result = builder(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSounds).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::KinoviSunoSounds(_))
|
||||
@@ -220,7 +220,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn kinovi_suno_remix_dispatches_to_draft() {
|
||||
let result = builder_with_audio_ref(RouterProvider::KinoviWeb, RouterAudioModel::SunoRemix).build2().expect("build");
|
||||
let result = builder_with_audio_ref(RouterProvider::Seedance2Pro, RouterAudioModel::SunoRemix).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Draft(AudioGenerationDraftRequest::KinoviSunoRemix(_))
|
||||
@@ -229,7 +229,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn kinovi_suno_sample_dispatches_to_draft() {
|
||||
let result = builder_with_audio_ref(RouterProvider::KinoviWeb, RouterAudioModel::SunoSample).build2().expect("build");
|
||||
let result = builder_with_audio_ref(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSample).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Draft(AudioGenerationDraftRequest::KinoviSunoSample(_))
|
||||
@@ -242,7 +242,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn kinovi_seed_audio_1p0_is_unsupported() {
|
||||
let result = builder(RouterProvider::KinoviWeb, RouterAudioModel::SeedAudio1p0).build2();
|
||||
let result = builder(RouterProvider::Seedance2Pro, RouterAudioModel::SeedAudio1p0).build2();
|
||||
assert!(matches!(result, Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(_))));
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -10,7 +10,7 @@ pub struct ArtcraftAudioResponsePayload {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KinoviWebAudioResponsePayload {
|
||||
pub struct Seedance2proAudioResponsePayload {
|
||||
pub order_id: String,
|
||||
pub task_id: String,
|
||||
}
|
||||
@@ -37,7 +37,7 @@ pub struct FalAudioResponsePayload {
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum GenerateAudioResponse {
|
||||
Artcraft(ArtcraftAudioResponsePayload),
|
||||
KinoviWeb(KinoviWebAudioResponsePayload),
|
||||
Seedance2Pro(Seedance2proAudioResponsePayload),
|
||||
Fal(FalAudioResponsePayload),
|
||||
}
|
||||
|
||||
@@ -49,9 +49,9 @@ impl GenerateAudioResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_kinovi_web_payload(&self) -> Option<KinoviWebAudioResponsePayload> {
|
||||
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proAudioResponsePayload> {
|
||||
match self {
|
||||
Self::KinoviWeb(p) => Some(p.clone()),
|
||||
Self::Seedance2Pro(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -5,14 +5,14 @@
|
||||
//! draft `finalize()` phase. The upload plumbing is shared with the Kinovi
|
||||
//! video provider (`generate_video::providers::kinovi::upload`).
|
||||
|
||||
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
|
||||
use crate::generate::generate_video::providers::kinovi::upload::upload_to_kinovi_web;
|
||||
use crate::generate::generate_video::providers::kinovi::upload::upload_to_seedance2pro;
|
||||
|
||||
/// A single unresolved audio reference: either an ArtCraft/public URL or a
|
||||
/// media file token that resolves to one via the draft context's map.
|
||||
@@ -55,7 +55,7 @@ pub(crate) fn require_single_audio_ref(
|
||||
/// Resolve a single audio reference to a source URL and re-upload it to the
|
||||
/// Kinovi CDN, returning the Kinovi CDN URL.
|
||||
pub(crate) async fn resolve_and_upload_audio_ref(
|
||||
session: &KinoviWebSession,
|
||||
session: &Seedance2ProSession,
|
||||
audio_ref: &SingleAudioRef,
|
||||
draft_context: &AudioGenerationDraftContext<'_>,
|
||||
) -> Result<String, ArtcraftRouterError> {
|
||||
@@ -71,7 +71,7 @@ pub(crate) async fn resolve_and_upload_audio_ref(
|
||||
})?
|
||||
}
|
||||
};
|
||||
upload_to_kinovi_web(session, &source_url).await
|
||||
upload_to_seedance2pro(session, &source_url).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
|
||||
use seedance2pro_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
@@ -119,7 +119,7 @@ mod tests {
|
||||
fn base_builder() -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoMusic,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("a song about corgis".to_string()),
|
||||
style_prompt: Some("sea shanty".to_string()),
|
||||
..Default::default()
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
|
||||
use seedance2pro_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
|
||||
|
||||
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_music::request::KinoviSunoMusicRequestState;
|
||||
@@ -13,7 +13,7 @@ impl KinoviSunoMusicCostState {
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
|
||||
// Cost math is owned by kinovi_web_client's binding — the router just
|
||||
// Cost math is owned by seedance2pro_client's binding — the router just
|
||||
// forwards the result so router cost ≡ binding cost by construction.
|
||||
let pricing_request = GenerateSunoMusicRequest {
|
||||
prompt: String::new(),
|
||||
@@ -54,7 +54,7 @@ mod tests {
|
||||
fn cost_from_built_request_is_seven_cents() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoMusic,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("a song".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
+17
-17
@@ -1,12 +1,12 @@
|
||||
use kinovi_web_client::generate::audio::generate_suno_music::{
|
||||
use seedance2pro_client::generate::audio::generate_suno_music::{
|
||||
generate_suno_music, GenerateSunoMusicArgs, GenerateSunoMusicRequest,
|
||||
};
|
||||
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_audio::generate_audio_response::{
|
||||
GenerateAudioResponse, KinoviWebAudioResponsePayload,
|
||||
GenerateAudioResponse, Seedance2proAudioResponsePayload,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -16,7 +16,7 @@ pub struct KinoviSunoMusicRequestState {
|
||||
}
|
||||
|
||||
impl KinoviSunoMusicRequestState {
|
||||
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
let session = &client.session;
|
||||
|
||||
let args = GenerateSunoMusicArgs {
|
||||
@@ -27,9 +27,9 @@ impl KinoviSunoMusicRequestState {
|
||||
|
||||
let response = generate_suno_music(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(GenerateAudioResponse::KinoviWeb(KinoviWebAudioResponsePayload {
|
||||
Ok(GenerateAudioResponse::Seedance2Pro(Seedance2proAudioResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
}))
|
||||
@@ -38,12 +38,12 @@ impl KinoviSunoMusicRequestState {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
@@ -56,7 +56,7 @@ mod tests {
|
||||
style_prompt: Some("Sea shanty, folk".to_string()),
|
||||
..suno_music_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
|
||||
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ mod tests {
|
||||
is_instrumental: Some(true),
|
||||
..suno_music_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
|
||||
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
@@ -78,20 +78,20 @@ mod tests {
|
||||
fn suno_music_builder() -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoMusic,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_kinovi_web_client() -> RouterClient {
|
||||
fn get_seedance2pro_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
|
||||
.expect("Failed to read kinovi_web cookies");
|
||||
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
|
||||
.expect("Failed to read seedance2pro cookies");
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
|
||||
let client = get_kinovi_web_client();
|
||||
let client = get_seedance2pro_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
@@ -102,7 +102,7 @@ mod tests {
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateAudioResponse::KinoviWeb(p) => {
|
||||
GenerateAudioResponse::Seedance2Pro(p) => {
|
||||
println!("task_id={}, order_id={}", p.task_id, p.order_id);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
|
||||
+1
-1
@@ -168,7 +168,7 @@ mod tests {
|
||||
fn base_builder() -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoRemix,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("make this electronic".to_string()),
|
||||
style_prompt: Some("EDM style".to_string()),
|
||||
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.mp3".to_string()])),
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::audio::generate_suno_remix::{
|
||||
use seedance2pro_client::generate::audio::generate_suno_remix::{
|
||||
GenerateSunoRemixRequest, KinoviSunoRemixSource,
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ impl KinoviSunoRemixCostState {
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
|
||||
// Cost math is owned by kinovi_web_client's binding — the router just
|
||||
// Cost math is owned by seedance2pro_client's binding — the router just
|
||||
// forwards the result so router cost ≡ binding cost by construction.
|
||||
let pricing_request = GenerateSunoRemixRequest {
|
||||
prompt: String::new(),
|
||||
@@ -63,7 +63,7 @@ mod tests {
|
||||
fn cost_from_draft_is_seven_cents() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoRemix,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("make this electronic".to_string()),
|
||||
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.mp3".to_string()])),
|
||||
..Default::default()
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::audio::generate_suno_remix::{
|
||||
use seedance2pro_client::generate::audio::generate_suno_remix::{
|
||||
GenerateSunoRemixRequest, KinoviSunoRemixSource,
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ impl KinoviSunoRemixDraftState {
|
||||
&mut self,
|
||||
draft_context: &AudioGenerationDraftContext<'_>,
|
||||
) -> Result<KinoviSunoRemixRequestState, ArtcraftRouterError> {
|
||||
let client = draft_context.get_kinovi_web_client_ref()?;
|
||||
let client = draft_context.get_seedance2pro_client_ref()?;
|
||||
let session = &client.session;
|
||||
|
||||
let audio_source = self.audio_source.take().ok_or_else(|| {
|
||||
|
||||
+19
-19
@@ -1,12 +1,12 @@
|
||||
use kinovi_web_client::generate::audio::generate_suno_remix::{
|
||||
use seedance2pro_client::generate::audio::generate_suno_remix::{
|
||||
generate_suno_remix, GenerateSunoRemixArgs, GenerateSunoRemixRequest,
|
||||
};
|
||||
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_audio::generate_audio_response::{
|
||||
GenerateAudioResponse, KinoviWebAudioResponsePayload,
|
||||
GenerateAudioResponse, Seedance2proAudioResponsePayload,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -17,7 +17,7 @@ pub struct KinoviSunoRemixRequestState {
|
||||
}
|
||||
|
||||
impl KinoviSunoRemixRequestState {
|
||||
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
let session = &client.session;
|
||||
|
||||
let args = GenerateSunoRemixArgs {
|
||||
@@ -28,9 +28,9 @@ impl KinoviSunoRemixRequestState {
|
||||
|
||||
let response = generate_suno_remix(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(GenerateAudioResponse::KinoviWeb(KinoviWebAudioResponsePayload {
|
||||
Ok(GenerateAudioResponse::Seedance2Pro(Seedance2proAudioResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
}))
|
||||
@@ -39,16 +39,16 @@ impl KinoviSunoRemixRequestState {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
|
||||
use kinovi_web_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
|
||||
use kinovi_web_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
|
||||
use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
|
||||
use test_utils::test_file_path::test_file_path;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
@@ -59,13 +59,13 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore] // Sends a real generation to Kinovi; costs credits. Requires a local audio file.
|
||||
async fn remix_uploaded_audio() {
|
||||
let client = get_kinovi_web_client();
|
||||
let client = get_seedance2pro_client();
|
||||
let audio_url = upload_test_audio(&client).await;
|
||||
println!("Uploaded audio: {}", audio_url);
|
||||
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoRemix,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("Make this electronic".to_string()),
|
||||
style_prompt: Some("EDM style".to_string()),
|
||||
audio_references: Some(AudioListRef::Urls(vec![audio_url])),
|
||||
@@ -73,17 +73,17 @@ mod tests {
|
||||
};
|
||||
|
||||
let response = run_pipeline(&client, builder).await;
|
||||
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
|
||||
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn get_kinovi_web_client() -> RouterClient {
|
||||
fn get_seedance2pro_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
|
||||
.expect("Failed to read kinovi_web cookies");
|
||||
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
|
||||
.expect("Failed to read seedance2pro cookies");
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
|
||||
}
|
||||
|
||||
async fn run_pipeline(client: &RouterClient, builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
|
||||
@@ -102,7 +102,7 @@ mod tests {
|
||||
let response = request.send_request(client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateAudioResponse::KinoviWeb(p) => {
|
||||
GenerateAudioResponse::Seedance2Pro(p) => {
|
||||
println!("task_id={}, order_id={}", p.task_id, p.order_id);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
@@ -112,7 +112,7 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn upload_test_audio(client: &RouterClient) -> String {
|
||||
let session = &client.get_kinovi_web_client_ref().expect("kinovi_web client").session;
|
||||
let session = &client.get_seedance2pro_client_ref().expect("seedance2pro client").session;
|
||||
|
||||
let audio_path = test_file_path(TEST_AUDIO_PATH).expect("test audio should exist");
|
||||
let audio_bytes = std::fs::read(&audio_path).expect("read test audio");
|
||||
|
||||
+1
-1
@@ -162,7 +162,7 @@ mod tests {
|
||||
fn base_builder() -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoSample,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("mystical RPG adventure".to_string()),
|
||||
style_prompt: Some("fantasy video game".to_string()),
|
||||
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.aac".to_string()])),
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::audio::generate_suno_sample::GenerateSunoSampleRequest;
|
||||
use seedance2pro_client::generate::audio::generate_suno_sample::GenerateSunoSampleRequest;
|
||||
|
||||
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_sample::draft::KinoviSunoSampleDraftState;
|
||||
@@ -18,7 +18,7 @@ impl KinoviSunoSampleCostState {
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
|
||||
// Cost math is owned by kinovi_web_client's binding — the router just
|
||||
// Cost math is owned by seedance2pro_client's binding — the router just
|
||||
// forwards the result so router cost ≡ binding cost by construction.
|
||||
let pricing_request = GenerateSunoSampleRequest {
|
||||
prompt: String::new(),
|
||||
@@ -63,7 +63,7 @@ mod tests {
|
||||
fn cost_from_draft_is_seven_cents() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoSample,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("mystical RPG adventure".to_string()),
|
||||
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.aac".to_string()])),
|
||||
..Default::default()
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::audio::generate_suno_sample::GenerateSunoSampleRequest;
|
||||
use seedance2pro_client::generate::audio::generate_suno_sample::GenerateSunoSampleRequest;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
|
||||
@@ -34,7 +34,7 @@ impl KinoviSunoSampleDraftState {
|
||||
&mut self,
|
||||
draft_context: &AudioGenerationDraftContext<'_>,
|
||||
) -> Result<KinoviSunoSampleRequestState, ArtcraftRouterError> {
|
||||
let client = draft_context.get_kinovi_web_client_ref()?;
|
||||
let client = draft_context.get_seedance2pro_client_ref()?;
|
||||
let session = &client.session;
|
||||
|
||||
let audio_source = self.audio_source.take().ok_or_else(|| {
|
||||
|
||||
+19
-19
@@ -1,12 +1,12 @@
|
||||
use kinovi_web_client::generate::audio::generate_suno_sample::{
|
||||
use seedance2pro_client::generate::audio::generate_suno_sample::{
|
||||
generate_suno_sample, GenerateSunoSampleArgs, GenerateSunoSampleRequest,
|
||||
};
|
||||
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_audio::generate_audio_response::{
|
||||
GenerateAudioResponse, KinoviWebAudioResponsePayload,
|
||||
GenerateAudioResponse, Seedance2proAudioResponsePayload,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -17,7 +17,7 @@ pub struct KinoviSunoSampleRequestState {
|
||||
}
|
||||
|
||||
impl KinoviSunoSampleRequestState {
|
||||
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
let session = &client.session;
|
||||
|
||||
let args = GenerateSunoSampleArgs {
|
||||
@@ -28,9 +28,9 @@ impl KinoviSunoSampleRequestState {
|
||||
|
||||
let response = generate_suno_sample(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(GenerateAudioResponse::KinoviWeb(KinoviWebAudioResponsePayload {
|
||||
Ok(GenerateAudioResponse::Seedance2Pro(Seedance2proAudioResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
}))
|
||||
@@ -39,16 +39,16 @@ impl KinoviSunoSampleRequestState {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
|
||||
use kinovi_web_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
|
||||
use kinovi_web_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
|
||||
use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
|
||||
use test_utils::test_file_path::test_file_path;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
@@ -59,13 +59,13 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore] // Sends a real generation to Kinovi; costs credits. Requires a local audio file.
|
||||
async fn sample_instrumental() {
|
||||
let client = get_kinovi_web_client();
|
||||
let client = get_seedance2pro_client();
|
||||
let audio_url = upload_test_audio(&client).await;
|
||||
println!("Uploaded audio: {}", audio_url);
|
||||
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoSample,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("Mystical RPG adventure, make it have a grand climax".to_string()),
|
||||
style_prompt: Some("Fantasy video game score".to_string()),
|
||||
is_instrumental: Some(true),
|
||||
@@ -74,17 +74,17 @@ mod tests {
|
||||
};
|
||||
|
||||
let response = run_pipeline(&client, builder).await;
|
||||
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
|
||||
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn get_kinovi_web_client() -> RouterClient {
|
||||
fn get_seedance2pro_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
|
||||
.expect("Failed to read kinovi_web cookies");
|
||||
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
|
||||
.expect("Failed to read seedance2pro cookies");
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
|
||||
}
|
||||
|
||||
async fn run_pipeline(client: &RouterClient, builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
|
||||
@@ -103,7 +103,7 @@ mod tests {
|
||||
let response = request.send_request(client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateAudioResponse::KinoviWeb(p) => {
|
||||
GenerateAudioResponse::Seedance2Pro(p) => {
|
||||
println!("task_id={}, order_id={}", p.task_id, p.order_id);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
@@ -113,7 +113,7 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn upload_test_audio(client: &RouterClient) -> String {
|
||||
let session = &client.get_kinovi_web_client_ref().expect("kinovi_web client").session;
|
||||
let session = &client.get_seedance2pro_client_ref().expect("seedance2pro client").session;
|
||||
|
||||
let audio_path = test_file_path(TEST_AUDIO_PATH).expect("test audio should exist");
|
||||
let audio_bytes = std::fs::read(&audio_path).expect("read test audio");
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
use enums::common::generation::common_musical_key::CommonMusicalKey;
|
||||
use kinovi_web_client::generate::audio::generate_suno_sound::{
|
||||
use seedance2pro_client::generate::audio::generate_suno_sound::{
|
||||
GenerateSunoSoundRequest, KinoviSunoSoundKey, KinoviSunoSoundType,
|
||||
};
|
||||
|
||||
@@ -201,7 +201,7 @@ mod tests {
|
||||
fn base_builder() -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoSounds,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("rain sound effects".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::audio::generate_suno_sound::{
|
||||
use seedance2pro_client::generate::audio::generate_suno_sound::{
|
||||
GenerateSunoSoundRequest, KinoviSunoSoundKey, KinoviSunoSoundType,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ impl KinoviSunoSoundsCostState {
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
|
||||
// Cost math is owned by kinovi_web_client's binding — the router just
|
||||
// Cost math is owned by seedance2pro_client's binding — the router just
|
||||
// forwards the result so router cost ≡ binding cost by construction.
|
||||
let pricing_request = GenerateSunoSoundRequest {
|
||||
prompt: String::new(),
|
||||
@@ -57,7 +57,7 @@ mod tests {
|
||||
fn cost_from_built_request_is_two_cents() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoSounds,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("thunder".to_string()),
|
||||
is_loopable: Some(true),
|
||||
bpm: Some(60),
|
||||
|
||||
+17
-17
@@ -1,12 +1,12 @@
|
||||
use kinovi_web_client::generate::audio::generate_suno_sound::{
|
||||
use seedance2pro_client::generate::audio::generate_suno_sound::{
|
||||
generate_suno_sound, GenerateSunoSoundArgs, GenerateSunoSoundRequest,
|
||||
};
|
||||
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_audio::generate_audio_response::{
|
||||
GenerateAudioResponse, KinoviWebAudioResponsePayload,
|
||||
GenerateAudioResponse, Seedance2proAudioResponsePayload,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -16,7 +16,7 @@ pub struct KinoviSunoSoundsRequestState {
|
||||
}
|
||||
|
||||
impl KinoviSunoSoundsRequestState {
|
||||
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
let session = &client.session;
|
||||
|
||||
let args = GenerateSunoSoundArgs {
|
||||
@@ -27,9 +27,9 @@ impl KinoviSunoSoundsRequestState {
|
||||
|
||||
let response = generate_suno_sound(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(GenerateAudioResponse::KinoviWeb(KinoviWebAudioResponsePayload {
|
||||
Ok(GenerateAudioResponse::Seedance2Pro(Seedance2proAudioResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
}))
|
||||
@@ -39,12 +39,12 @@ impl KinoviSunoSoundsRequestState {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_musical_key::CommonMusicalKey;
|
||||
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
@@ -56,7 +56,7 @@ mod tests {
|
||||
prompt: Some("A heavy castle door creaking open".to_string()),
|
||||
..suno_sounds_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
|
||||
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ mod tests {
|
||||
musical_key: Some(CommonMusicalKey::AMinor),
|
||||
..suno_sounds_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
|
||||
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
@@ -79,20 +79,20 @@ mod tests {
|
||||
fn suno_sounds_builder() -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoSounds,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_kinovi_web_client() -> RouterClient {
|
||||
fn get_seedance2pro_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
|
||||
.expect("Failed to read kinovi_web cookies");
|
||||
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
|
||||
.expect("Failed to read seedance2pro cookies");
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
|
||||
let client = get_kinovi_web_client();
|
||||
let client = get_seedance2pro_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
@@ -103,7 +103,7 @@ mod tests {
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateAudioResponse::KinoviWeb(p) => {
|
||||
GenerateAudioResponse::Seedance2Pro(p) => {
|
||||
println!("task_id={}, order_id={}", p.task_id, p.order_id);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
|
||||
+6
-6
@@ -146,12 +146,12 @@ impl GenerateImageRequestBuilder {
|
||||
(RouterProvider::Fal, RouterImageModel::QwenEdit2511Angles) => build_fal_qwen_edit_2511_angles(self),
|
||||
(RouterProvider::Fal, RouterImageModel::Flux2LoraAngles) => build_fal_flux_2_lora_angles(self),
|
||||
|
||||
// Kinovi / KinoviWeb (Midjourney + Seedream)
|
||||
(RouterProvider::KinoviWeb, RouterImageModel::Midjourney7) => build_kinovi_midjourney_7(self),
|
||||
(RouterProvider::KinoviWeb, RouterImageModel::Midjourney7Niji) => build_kinovi_midjourney_7_niji(self),
|
||||
(RouterProvider::KinoviWeb, RouterImageModel::Midjourney8) => build_kinovi_midjourney_8(self),
|
||||
(RouterProvider::KinoviWeb, RouterImageModel::Seedream5p0Pro) => build_kinovi_seedream_5p0_pro(self),
|
||||
(RouterProvider::KinoviWeb, RouterImageModel::Seedream5p0ProUltra) => build_kinovi_seedream_5p0_pro(self),
|
||||
// Kinovi / Seedance2Pro (Midjourney + Seedream)
|
||||
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney7) => build_kinovi_midjourney_7(self),
|
||||
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney7Niji) => build_kinovi_midjourney_7_niji(self),
|
||||
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney8) => build_kinovi_midjourney_8(self),
|
||||
(RouterProvider::Seedance2Pro, RouterImageModel::Seedream5p0Pro) => build_kinovi_seedream_5p0_pro(self),
|
||||
(RouterProvider::Seedance2Pro, RouterImageModel::Seedream5p0ProUltra) => build_kinovi_seedream_5p0_pro(self),
|
||||
|
||||
_ => self.unsupported_provider_and_model(),
|
||||
}
|
||||
|
||||
+6
-6
@@ -25,11 +25,11 @@ pub struct FalImageResponsePayload {
|
||||
pub maybe_outbound_request: Option<Arc<dyn Debug + Send + Sync>>,
|
||||
}
|
||||
|
||||
/// Response from the KinoviWeb/Kinovi provider (used for Midjourney
|
||||
/// image generation). Mirrors `KinoviWebVideoResponsePayload` on the
|
||||
/// Response from the Seedance2Pro/Kinovi provider (used for Midjourney
|
||||
/// image generation). Mirrors `Seedance2proVideoResponsePayload` on the
|
||||
/// video side.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KinoviWebImageResponsePayload {
|
||||
pub struct Seedance2proImageResponsePayload {
|
||||
pub order_id: String,
|
||||
pub task_id: String,
|
||||
pub maybe_order_ids: Option<Vec<String>>,
|
||||
@@ -40,7 +40,7 @@ pub struct KinoviWebImageResponsePayload {
|
||||
pub enum GenerateImageResponse {
|
||||
Artcraft(ArtcraftImageResponsePayload),
|
||||
Fal(FalImageResponsePayload),
|
||||
KinoviWeb(KinoviWebImageResponsePayload),
|
||||
Seedance2Pro(Seedance2proImageResponsePayload),
|
||||
}
|
||||
|
||||
impl GenerateImageResponse {
|
||||
@@ -58,9 +58,9 @@ impl GenerateImageResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_kinovi_web_payload(&self) -> Option<KinoviWebImageResponsePayload> {
|
||||
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proImageResponsePayload> {
|
||||
match self {
|
||||
Self::KinoviWeb(p) => Some(p.clone()),
|
||||
Self::Seedance2Pro(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -17,7 +17,7 @@ use crate::generate::generate_image::providers::kinovi::seedream_5p0_pro::draft:
|
||||
/// Drafts hold validated/planned parameters but may still need media
|
||||
/// resolution (e.g. uploading local files to a provider) before they can
|
||||
/// be sent. Today only the Kinovi models need this — when the caller
|
||||
/// supplies reference images, we must upload them to the KinoviWeb
|
||||
/// supplies reference images, we must upload them to the Seedance2Pro
|
||||
/// CDN before sending the request.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ImageGenerationDraftRequest {
|
||||
@@ -30,10 +30,10 @@ pub enum ImageGenerationDraftRequest {
|
||||
impl ImageGenerationDraftRequest {
|
||||
pub fn get_provider(&self) -> RouterProvider {
|
||||
match self {
|
||||
Self::KinoviMidjourney7(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviMidjourney7Niji(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviMidjourney8(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviSeedream5p0Pro(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviMidjourney7(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviMidjourney7Niji(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviMidjourney8(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSeedream5p0Pro(_) => RouterProvider::Seedance2Pro,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -4,21 +4,21 @@ use std::fmt::Debug;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
/// Context passed when finalizing an image draft into a concrete request.
|
||||
///
|
||||
/// Drafts only exist for providers that need to upload assets before
|
||||
/// sending the request. Today that's the KinoviWeb/Kinovi pipeline
|
||||
/// sending the request. Today that's the Seedance2Pro/Kinovi pipeline
|
||||
/// (used for Midjourney image generation with reference images); other
|
||||
/// providers — Artcraft, Fal — accept image URLs directly and skip the
|
||||
/// draft phase entirely.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ImageGenerationDraftContext<'a> {
|
||||
/// Optional: the router client, needed for providers that require
|
||||
/// authenticated calls (e.g. KinoviWeb/Kinovi uploads).
|
||||
/// authenticated calls (e.g. Seedance2Pro/Kinovi uploads).
|
||||
pub client: Option<&'a RouterClient>,
|
||||
|
||||
/// Optional: a map of Media File Tokens to their ArtCraft URLs.
|
||||
@@ -29,9 +29,9 @@ pub struct ImageGenerationDraftContext<'a> {
|
||||
}
|
||||
|
||||
impl<'a> ImageGenerationDraftContext<'a> {
|
||||
pub fn get_kinovi_web_client_ref(&self) -> Result<&RouterKinoviWebClient, ArtcraftRouterError> {
|
||||
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ArtcraftRouterError> {
|
||||
let client = self.client.ok_or(ArtcraftRouterError::Client(ClientError::RouterClientNotProvided))?;
|
||||
client.get_kinovi_web_client_ref()
|
||||
client.get_seedance2pro_client_ref()
|
||||
.map_err(ArtcraftRouterError::Client)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -123,7 +123,7 @@ pub enum ImageGenerationRequest {
|
||||
FalQwenEdit2511Angles(FalQwenEdit2511AnglesRequestState),
|
||||
FalFlux2LoraAngles(FalFlux2LoraAnglesRequestState),
|
||||
|
||||
// ── Kinovi / KinoviWeb provider (Midjourney + Seedream image generation) ──
|
||||
// ── Kinovi / Seedance2Pro provider (Midjourney + Seedream image generation) ──
|
||||
KinoviMidjourney7(KinoviMidjourney7RequestState),
|
||||
KinoviMidjourney7Niji(KinoviMidjourney7NijiRequestState),
|
||||
KinoviMidjourney8(KinoviMidjourney8RequestState),
|
||||
@@ -170,10 +170,10 @@ impl ImageGenerationRequest {
|
||||
Self::FalQwenEdit2511Angles(_) => RouterProvider::Fal,
|
||||
Self::FalFlux2LoraAngles(_) => RouterProvider::Fal,
|
||||
|
||||
Self::KinoviMidjourney7(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviMidjourney7Niji(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviMidjourney8(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviSeedream5p0Pro(_) => RouterProvider::KinoviWeb,
|
||||
Self::KinoviMidjourney7(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviMidjourney7Niji(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviMidjourney8(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSeedream5p0Pro(_) => RouterProvider::Seedance2Pro,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,24 +416,24 @@ impl ImageGenerationRequest {
|
||||
request.send(fal_client).await
|
||||
}
|
||||
|
||||
// ── Kinovi / KinoviWeb (Midjourney + Seedream) ──
|
||||
// ── Kinovi / Seedance2Pro (Midjourney + Seedream) ──
|
||||
Self::KinoviMidjourney7(request) => {
|
||||
let seedance_client = client.get_kinovi_web_client_ref()
|
||||
let seedance_client = client.get_seedance2pro_client_ref()
|
||||
.map_err(ArtcraftRouterError::Client)?;
|
||||
request.send(seedance_client).await
|
||||
}
|
||||
Self::KinoviMidjourney7Niji(request) => {
|
||||
let seedance_client = client.get_kinovi_web_client_ref()
|
||||
let seedance_client = client.get_seedance2pro_client_ref()
|
||||
.map_err(ArtcraftRouterError::Client)?;
|
||||
request.send(seedance_client).await
|
||||
}
|
||||
Self::KinoviMidjourney8(request) => {
|
||||
let seedance_client = client.get_kinovi_web_client_ref()
|
||||
let seedance_client = client.get_seedance2pro_client_ref()
|
||||
.map_err(ArtcraftRouterError::Client)?;
|
||||
request.send(seedance_client).await
|
||||
}
|
||||
Self::KinoviSeedream5p0Pro(request) => {
|
||||
let seedance_client = client.get_kinovi_web_client_ref()
|
||||
let seedance_client = client.get_seedance2pro_client_ref()
|
||||
.map_err(ArtcraftRouterError::Client)?;
|
||||
request.send(seedance_client).await
|
||||
}
|
||||
|
||||
+2
-1
@@ -2,7 +2,8 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::providers::artcraft::flux_2_lora_angles::request::ArtcraftFlux2LoraAnglesRequestState;
|
||||
|
||||
/// Cost state for Artcraft Flux 2 LoRA Angles. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_flux_2_lora_angles`): 4¢ per output image.
|
||||
/// (`estimate_image_cost_artcraft_flux_2_lora_angles`): 4¢ per output image
|
||||
/// (Artcraft markup over Fal's 2¢/image base rate).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFlux2LoraAnglesCostState {
|
||||
pub num_images: u16,
|
||||
|
||||
+5
-7
@@ -16,9 +16,7 @@ impl ArtcraftQwenEdit2511AnglesCostState {
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
// 4.6¢ per output image (held in hundredths of a cent, rounded up to
|
||||
// whole cents across the batch).
|
||||
let cost_in_usd_cents = (460u64 * self.num_images as u64).div_ceil(100);
|
||||
let cost_in_usd_cents = 4u64 * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
@@ -61,14 +59,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_costs_5_cents() { assert_eq!(cost_cents(1), 5); }
|
||||
fn one_image_costs_4_cents() { assert_eq!(cost_cents(1), 4); }
|
||||
|
||||
#[test]
|
||||
fn two_images_costs_10_cents() { assert_eq!(cost_cents(2), 10); }
|
||||
fn two_images_costs_8_cents() { assert_eq!(cost_cents(2), 8); }
|
||||
|
||||
#[test]
|
||||
fn three_images_costs_14_cents() { assert_eq!(cost_cents(3), 14); }
|
||||
fn three_images_costs_12_cents() { assert_eq!(cost_cents(3), 12); }
|
||||
|
||||
#[test]
|
||||
fn four_images_costs_19_cents() { assert_eq!(cost_cents(4), 19); }
|
||||
fn four_images_costs_16_cents() { assert_eq!(cost_cents(4), 16); }
|
||||
}
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn price_covers_kinovi_cost() {
|
||||
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
|
||||
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
|
||||
KinoviSeedream5p0ProBatchCount, KinoviSeedream5p0ProResolution,
|
||||
};
|
||||
use crate::generate::generate_image::providers::kinovi::seedream_5p0_pro::cost::KinoviSeedream5p0ProCostState;
|
||||
|
||||
+1
-1
@@ -169,7 +169,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn price_covers_kinovi_cost() {
|
||||
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
|
||||
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
|
||||
KinoviSeedream5p0ProBatchCount, KinoviSeedream5p0ProResolution,
|
||||
};
|
||||
use crate::generate::generate_image::providers::kinovi::seedream_5p0_pro::cost::KinoviSeedream5p0ProCostState;
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7::{
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7::{
|
||||
GenerateMidjourneyV7AspectRatio, GenerateMidjourneyV7Quality,
|
||||
GenerateMidjourneyV7Request, KinoviMidjourneyBatchCount,
|
||||
};
|
||||
@@ -50,7 +50,7 @@ pub fn build_kinovi_midjourney_7(
|
||||
}
|
||||
|
||||
// Image inputs present → must go through the draft phase so we can
|
||||
// upload each image to the KinoviWeb CDN before sending.
|
||||
// upload each image to the Seedance2Pro CDN before sending.
|
||||
let draft = KinoviMidjourney7DraftState {
|
||||
prompt,
|
||||
aspect_ratio,
|
||||
@@ -159,7 +159,7 @@ pub(crate) fn plan_batch_count(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7::KinoviMidjourneyBatchCount;
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7::KinoviMidjourneyBatchCount;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
@@ -178,7 +178,7 @@ mod tests {
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: RouterImageModel::Midjourney7,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("a corgi astronaut".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
@@ -291,7 +291,7 @@ mod tests {
|
||||
|
||||
mod aspect_ratio_tests {
|
||||
use super::*;
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7AspectRatio as Ar;
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7AspectRatio as Ar;
|
||||
|
||||
fn aspect_ratio_for(input: Option<RouterAspectRatio>) -> Ar {
|
||||
let builder = GenerateImageRequestBuilder { aspect_ratio: input, ..base_builder() };
|
||||
@@ -334,7 +334,7 @@ mod tests {
|
||||
|
||||
mod quality_tests {
|
||||
use super::*;
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7Quality as Q;
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7Quality as Q;
|
||||
|
||||
fn quality_for(input: Option<RouterQuality>) -> Option<Q> {
|
||||
let builder = GenerateImageRequestBuilder { quality: input, ..base_builder() };
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7::{
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7::{
|
||||
GenerateMidjourneyV7AspectRatio, GenerateMidjourneyV7Request, KinoviMidjourneyBatchCount,
|
||||
};
|
||||
|
||||
@@ -171,7 +171,7 @@ mod tests {
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: RouterImageModel::Midjourney7,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("test".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7::{
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7::{
|
||||
GenerateMidjourneyV7AspectRatio, GenerateMidjourneyV7Quality,
|
||||
GenerateMidjourneyV7Request, KinoviMidjourneyBatchCount,
|
||||
};
|
||||
@@ -17,7 +17,7 @@ pub struct KinoviMidjourney7DraftState {
|
||||
pub quality: Option<GenerateMidjourneyV7Quality>,
|
||||
pub batch_count: KinoviMidjourneyBatchCount,
|
||||
|
||||
/// Image references that still need to be uploaded to KinoviWeb CDN
|
||||
/// Image references that still need to be uploaded to Seedance2Pro CDN
|
||||
/// before the request is sendable. `None` once `to_request()` has run.
|
||||
pub unhandled_request_state: Option<KinoviMidjourney7RemainingItems>,
|
||||
}
|
||||
@@ -32,7 +32,7 @@ impl KinoviMidjourney7DraftState {
|
||||
&mut self,
|
||||
draft_context: &ImageGenerationDraftContext<'_>,
|
||||
) -> Result<KinoviMidjourney7RequestState, ArtcraftRouterError> {
|
||||
let client = draft_context.get_kinovi_web_client_ref()?;
|
||||
let client = draft_context.get_seedance2pro_client_ref()?;
|
||||
let session = &client.session;
|
||||
|
||||
let mut reference_image_urls = None;
|
||||
@@ -63,10 +63,10 @@ impl KinoviMidjourney7DraftState {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7AspectRatio;
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7AspectRatio;
|
||||
|
||||
// Note: most behavioral tests live in `build.rs` and `cost.rs`. The
|
||||
// `to_request()` method requires a live KinoviWeb client to upload
|
||||
// `to_request()` method requires a live Seedance2Pro client to upload
|
||||
// images, so its end-to-end behavior is exercised by the `request.rs`
|
||||
// #[ignore]'d integration tests rather than unit-tested here.
|
||||
|
||||
|
||||
+15
-15
@@ -1,12 +1,12 @@
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7::{
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7::{
|
||||
generate_midjourney_v7, GenerateMidjourneyV7Args, GenerateMidjourneyV7Request,
|
||||
};
|
||||
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
GenerateImageResponse, KinoviWebImageResponsePayload,
|
||||
GenerateImageResponse, Seedance2proImageResponsePayload,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -18,7 +18,7 @@ pub struct KinoviMidjourney7RequestState {
|
||||
impl KinoviMidjourney7RequestState {
|
||||
pub async fn send(
|
||||
&self,
|
||||
client: &RouterKinoviWebClient,
|
||||
client: &RouterSeedance2ProClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let args = GenerateMidjourneyV7Args {
|
||||
session: &client.session,
|
||||
@@ -28,9 +28,9 @@ impl KinoviMidjourney7RequestState {
|
||||
|
||||
let response = generate_midjourney_v7(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(GenerateImageResponse::KinoviWeb(KinoviWebImageResponsePayload {
|
||||
Ok(GenerateImageResponse::Seedance2Pro(Seedance2proImageResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
maybe_order_ids: response.order_ids,
|
||||
@@ -43,7 +43,7 @@ impl KinoviMidjourney7RequestState {
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
@@ -52,7 +52,7 @@ mod tests {
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
|
||||
@@ -64,7 +64,7 @@ mod tests {
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: RouterImageModel::Midjourney7,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("a serene mountain lake at sunrise, cinematic".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
@@ -82,9 +82,9 @@ mod tests {
|
||||
|
||||
fn get_kinovi_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
|
||||
.expect("Failed to read kinovi_web cookies");
|
||||
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
|
||||
.expect("Failed to read seedance2pro cookies");
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -98,9 +98,9 @@ mod tests {
|
||||
_ => panic!("expected direct Request for text-to-image"),
|
||||
};
|
||||
|
||||
let response = request.send(client.get_kinovi_web_client_ref().unwrap())
|
||||
let response = request.send(client.get_seedance2pro_client_ref().unwrap())
|
||||
.await.expect("send should succeed");
|
||||
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
|
||||
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
|
||||
println!("v7 t2i — task_id={}, order_id={}", payload.task_id, payload.order_id);
|
||||
assert!(!payload.task_id.is_empty());
|
||||
}
|
||||
@@ -135,7 +135,7 @@ mod tests {
|
||||
.finalize(ctx).await.expect("finalize");
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request");
|
||||
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
|
||||
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
|
||||
println!("v7 with ref — task_id={}, order_id={}", payload.task_id, payload.order_id);
|
||||
assert!(!payload.task_id.is_empty());
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::{
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::{
|
||||
GenerateMidjourneyV7NijiAspectRatio, GenerateMidjourneyV7NijiQuality,
|
||||
GenerateMidjourneyV7NijiRequest, KinoviMidjourneyBatchCount,
|
||||
};
|
||||
@@ -147,7 +147,7 @@ pub(crate) fn plan_batch_count(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::KinoviMidjourneyBatchCount;
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::KinoviMidjourneyBatchCount;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
@@ -166,7 +166,7 @@ mod tests {
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: RouterImageModel::Midjourney7Niji,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("a magical anime forest scene".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
@@ -235,7 +235,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_passes_through_to_niji_enum() {
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::GenerateMidjourneyV7NijiAspectRatio as Ar;
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::GenerateMidjourneyV7NijiAspectRatio as Ar;
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(RouterAspectRatio::WideTwentyOneByNine),
|
||||
..base_builder()
|
||||
@@ -249,7 +249,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn quality_high_maps_to_full() {
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::GenerateMidjourneyV7NijiQuality as Q;
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::GenerateMidjourneyV7NijiQuality as Q;
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
quality: Some(RouterQuality::High),
|
||||
..base_builder()
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::{
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::{
|
||||
GenerateMidjourneyV7NijiAspectRatio, GenerateMidjourneyV7NijiRequest, KinoviMidjourneyBatchCount,
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::{
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::{
|
||||
GenerateMidjourneyV7NijiAspectRatio, GenerateMidjourneyV7NijiQuality,
|
||||
GenerateMidjourneyV7NijiRequest, KinoviMidjourneyBatchCount,
|
||||
};
|
||||
@@ -29,7 +29,7 @@ impl KinoviMidjourney7NijiDraftState {
|
||||
&mut self,
|
||||
draft_context: &ImageGenerationDraftContext<'_>,
|
||||
) -> Result<KinoviMidjourney7NijiRequestState, ArtcraftRouterError> {
|
||||
let client = draft_context.get_kinovi_web_client_ref()?;
|
||||
let client = draft_context.get_seedance2pro_client_ref()?;
|
||||
let session = &client.session;
|
||||
|
||||
let mut reference_image_urls = None;
|
||||
|
||||
+15
-15
@@ -1,12 +1,12 @@
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::{
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::{
|
||||
generate_midjourney_v7_niji, GenerateMidjourneyV7NijiArgs, GenerateMidjourneyV7NijiRequest,
|
||||
};
|
||||
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
GenerateImageResponse, KinoviWebImageResponsePayload,
|
||||
GenerateImageResponse, Seedance2proImageResponsePayload,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -17,7 +17,7 @@ pub struct KinoviMidjourney7NijiRequestState {
|
||||
impl KinoviMidjourney7NijiRequestState {
|
||||
pub async fn send(
|
||||
&self,
|
||||
client: &RouterKinoviWebClient,
|
||||
client: &RouterSeedance2ProClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let args = GenerateMidjourneyV7NijiArgs {
|
||||
session: &client.session,
|
||||
@@ -27,9 +27,9 @@ impl KinoviMidjourney7NijiRequestState {
|
||||
|
||||
let response = generate_midjourney_v7_niji(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(GenerateImageResponse::KinoviWeb(KinoviWebImageResponsePayload {
|
||||
Ok(GenerateImageResponse::Seedance2Pro(Seedance2proImageResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
maybe_order_ids: response.order_ids,
|
||||
@@ -42,7 +42,7 @@ impl KinoviMidjourney7NijiRequestState {
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
@@ -51,7 +51,7 @@ mod tests {
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
|
||||
use crate::generate::generate_image::image_generation_draft_context::ImageGenerationDraftContext;
|
||||
@@ -62,7 +62,7 @@ mod tests {
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: RouterImageModel::Midjourney7Niji,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("a magical anime fox spirit in a moonlit bamboo forest".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
@@ -80,9 +80,9 @@ mod tests {
|
||||
|
||||
fn get_kinovi_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
|
||||
.expect("Failed to read kinovi_web cookies");
|
||||
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
|
||||
.expect("Failed to read seedance2pro cookies");
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -96,9 +96,9 @@ mod tests {
|
||||
_ => panic!("expected direct Request for text-to-image"),
|
||||
};
|
||||
|
||||
let response = request.send(client.get_kinovi_web_client_ref().unwrap())
|
||||
let response = request.send(client.get_seedance2pro_client_ref().unwrap())
|
||||
.await.expect("send should succeed");
|
||||
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
|
||||
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
|
||||
println!("v7-niji t2i — task_id={}, order_id={}", payload.task_id, payload.order_id);
|
||||
assert!(!payload.task_id.is_empty());
|
||||
}
|
||||
@@ -133,7 +133,7 @@ mod tests {
|
||||
.finalize(ctx).await.expect("finalize");
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request");
|
||||
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
|
||||
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
|
||||
println!("v7-niji with ref — task_id={}, order_id={}", payload.task_id, payload.order_id);
|
||||
assert!(!payload.task_id.is_empty());
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v8::{
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v8::{
|
||||
GenerateMidjourneyV8AspectRatio, GenerateMidjourneyV8Quality,
|
||||
GenerateMidjourneyV8Request, KinoviMidjourneyBatchCount,
|
||||
};
|
||||
@@ -147,7 +147,7 @@ pub(crate) fn plan_batch_count(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v8::KinoviMidjourneyBatchCount;
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v8::KinoviMidjourneyBatchCount;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
@@ -164,7 +164,7 @@ mod tests {
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: RouterImageModel::Midjourney8,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("a spooky horror scene".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v8::{
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v8::{
|
||||
GenerateMidjourneyV8AspectRatio, GenerateMidjourneyV8Request, KinoviMidjourneyBatchCount,
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v8::{
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v8::{
|
||||
GenerateMidjourneyV8AspectRatio, GenerateMidjourneyV8Quality,
|
||||
GenerateMidjourneyV8Request, KinoviMidjourneyBatchCount,
|
||||
};
|
||||
@@ -29,7 +29,7 @@ impl KinoviMidjourney8DraftState {
|
||||
&mut self,
|
||||
draft_context: &ImageGenerationDraftContext<'_>,
|
||||
) -> Result<KinoviMidjourney8RequestState, ArtcraftRouterError> {
|
||||
let client = draft_context.get_kinovi_web_client_ref()?;
|
||||
let client = draft_context.get_seedance2pro_client_ref()?;
|
||||
let session = &client.session;
|
||||
|
||||
let mut reference_image_urls = None;
|
||||
|
||||
+15
-15
@@ -1,12 +1,12 @@
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v8::{
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v8::{
|
||||
generate_midjourney_v8, GenerateMidjourneyV8Args, GenerateMidjourneyV8Request,
|
||||
};
|
||||
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
GenerateImageResponse, KinoviWebImageResponsePayload,
|
||||
GenerateImageResponse, Seedance2proImageResponsePayload,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -17,7 +17,7 @@ pub struct KinoviMidjourney8RequestState {
|
||||
impl KinoviMidjourney8RequestState {
|
||||
pub async fn send(
|
||||
&self,
|
||||
client: &RouterKinoviWebClient,
|
||||
client: &RouterSeedance2ProClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let args = GenerateMidjourneyV8Args {
|
||||
session: &client.session,
|
||||
@@ -27,9 +27,9 @@ impl KinoviMidjourney8RequestState {
|
||||
|
||||
let response = generate_midjourney_v8(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(GenerateImageResponse::KinoviWeb(KinoviWebImageResponsePayload {
|
||||
Ok(GenerateImageResponse::Seedance2Pro(Seedance2proImageResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
maybe_order_ids: response.order_ids,
|
||||
@@ -42,7 +42,7 @@ impl KinoviMidjourney8RequestState {
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
@@ -51,7 +51,7 @@ mod tests {
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
|
||||
use crate::generate::generate_image::image_generation_draft_context::ImageGenerationDraftContext;
|
||||
@@ -62,7 +62,7 @@ mod tests {
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: RouterImageModel::Midjourney8,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("a spooky horror scene with ghosts in an abandoned mansion".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
@@ -80,9 +80,9 @@ mod tests {
|
||||
|
||||
fn get_kinovi_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
|
||||
.expect("Failed to read kinovi_web cookies");
|
||||
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
|
||||
.expect("Failed to read seedance2pro cookies");
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -96,9 +96,9 @@ mod tests {
|
||||
_ => panic!("expected direct Request for text-to-image"),
|
||||
};
|
||||
|
||||
let response = request.send(client.get_kinovi_web_client_ref().unwrap())
|
||||
let response = request.send(client.get_seedance2pro_client_ref().unwrap())
|
||||
.await.expect("send should succeed");
|
||||
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
|
||||
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
|
||||
println!("v8 t2i — task_id={}, order_id={}", payload.task_id, payload.order_id);
|
||||
assert!(!payload.task_id.is_empty());
|
||||
}
|
||||
@@ -133,7 +133,7 @@ mod tests {
|
||||
.finalize(ctx).await.expect("finalize");
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request");
|
||||
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
|
||||
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
|
||||
println!("v8 with ref — task_id={}, order_id={}", payload.task_id, payload.order_id);
|
||||
assert!(!payload.task_id.is_empty());
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,21 +1,21 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::providers::kinovi::upload::upload_to_kinovi_web;
|
||||
use crate::generate::generate_image::providers::kinovi::upload::upload_to_seedance2pro;
|
||||
|
||||
/// Resolve an [`ImageListRef`] to a list of public KinoviWeb CDN URLs,
|
||||
/// Resolve an [`ImageListRef`] to a list of public Seedance2Pro CDN URLs,
|
||||
/// uploading each image one at a time. Preserves order.
|
||||
///
|
||||
/// Returns `Ok(None)` when the input is `None` or an empty list — the
|
||||
/// caller can then omit the field from the wire request entirely. (The
|
||||
/// downstream Midjourney API rejects empty arrays.)
|
||||
pub(crate) async fn resolve_and_upload_image_list(
|
||||
session: &KinoviWebSession,
|
||||
session: &Seedance2ProSession,
|
||||
list: Option<ImageListRef>,
|
||||
maybe_map: Option<&HashMap<MediaFileToken, String>>,
|
||||
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
|
||||
@@ -29,7 +29,7 @@ pub(crate) async fn resolve_and_upload_image_list(
|
||||
|
||||
let mut uploaded = Vec::with_capacity(source_urls.len());
|
||||
for url in &source_urls {
|
||||
uploaded.push(upload_to_kinovi_web(session, url).await?);
|
||||
uploaded.push(upload_to_seedance2pro(session, url).await?);
|
||||
}
|
||||
Ok(Some(uploaded))
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
|
||||
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
|
||||
GenerateSeedream5p0ProRequest, KinoviSeedream5p0ProAspectRatio, KinoviSeedream5p0ProBatchCount,
|
||||
KinoviSeedream5p0ProResolution, MAX_REFERENCE_IMAGES,
|
||||
};
|
||||
@@ -67,7 +67,7 @@ fn has_image_inputs(image_inputs: Option<&ImageListRef>) -> bool {
|
||||
}
|
||||
|
||||
/// Reject over-limit reference lists before any uploads happen — the
|
||||
/// KinoviWeb client would reject them anyway, but only after the draft
|
||||
/// Seedance2Pro client would reject them anyway, but only after the draft
|
||||
/// stage has already uploaded every image to the Kinovi CDN.
|
||||
fn validate_reference_image_count(
|
||||
image_inputs: Option<&ImageListRef>,
|
||||
@@ -191,7 +191,7 @@ mod tests {
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: RouterImageModel::Seedream5p0Pro,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("an anime girl riding a dinosaur".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
|
||||
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
|
||||
GenerateSeedream5p0ProRequest, KinoviSeedream5p0ProAspectRatio, KinoviSeedream5p0ProBatchCount,
|
||||
KinoviSeedream5p0ProResolution,
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
|
||||
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
|
||||
GenerateSeedream5p0ProRequest, KinoviSeedream5p0ProAspectRatio, KinoviSeedream5p0ProBatchCount,
|
||||
KinoviSeedream5p0ProResolution,
|
||||
};
|
||||
@@ -29,7 +29,7 @@ impl KinoviSeedream5p0ProDraftState {
|
||||
&mut self,
|
||||
draft_context: &ImageGenerationDraftContext<'_>,
|
||||
) -> Result<KinoviSeedream5p0ProRequestState, ArtcraftRouterError> {
|
||||
let client = draft_context.get_kinovi_web_client_ref()?;
|
||||
let client = draft_context.get_seedance2pro_client_ref()?;
|
||||
let session = &client.session;
|
||||
|
||||
let mut reference_image_urls = None;
|
||||
|
||||
+15
-15
@@ -1,12 +1,12 @@
|
||||
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
|
||||
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
|
||||
generate_seedream_5p0_pro, GenerateSeedream5p0ProArgs, GenerateSeedream5p0ProRequest,
|
||||
};
|
||||
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
GenerateImageResponse, KinoviWebImageResponsePayload,
|
||||
GenerateImageResponse, Seedance2proImageResponsePayload,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -17,7 +17,7 @@ pub struct KinoviSeedream5p0ProRequestState {
|
||||
impl KinoviSeedream5p0ProRequestState {
|
||||
pub async fn send(
|
||||
&self,
|
||||
client: &RouterKinoviWebClient,
|
||||
client: &RouterSeedance2ProClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let args = GenerateSeedream5p0ProArgs {
|
||||
session: &client.session,
|
||||
@@ -27,9 +27,9 @@ impl KinoviSeedream5p0ProRequestState {
|
||||
|
||||
let response = generate_seedream_5p0_pro(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(GenerateImageResponse::KinoviWeb(KinoviWebImageResponsePayload {
|
||||
Ok(GenerateImageResponse::Seedance2Pro(Seedance2proImageResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
maybe_order_ids: response.order_ids,
|
||||
@@ -42,7 +42,7 @@ impl KinoviSeedream5p0ProRequestState {
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
@@ -52,7 +52,7 @@ mod tests {
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
|
||||
use crate::generate::generate_image::image_generation_draft_context::ImageGenerationDraftContext;
|
||||
@@ -63,7 +63,7 @@ mod tests {
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: RouterImageModel::Seedream5p0Pro,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("an anime girl riding a dinosaur through a neon city".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: Some(RouterResolution::OneK),
|
||||
@@ -81,9 +81,9 @@ mod tests {
|
||||
|
||||
fn get_kinovi_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
|
||||
.expect("Failed to read kinovi_web cookies");
|
||||
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
|
||||
.expect("Failed to read seedance2pro cookies");
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -97,9 +97,9 @@ mod tests {
|
||||
_ => panic!("expected direct Request for text-to-image"),
|
||||
};
|
||||
|
||||
let response = request.send(client.get_kinovi_web_client_ref().unwrap())
|
||||
let response = request.send(client.get_seedance2pro_client_ref().unwrap())
|
||||
.await.expect("send should succeed");
|
||||
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
|
||||
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
|
||||
println!("seedream 5.0 pro t2i — task_id={}, order_id={}", payload.task_id, payload.order_id);
|
||||
assert!(!payload.task_id.is_empty());
|
||||
}
|
||||
@@ -134,7 +134,7 @@ mod tests {
|
||||
.finalize(ctx).await.expect("finalize");
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request");
|
||||
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
|
||||
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
|
||||
println!("seedream 5.0 pro with ref — task_id={}, order_id={}", payload.task_id, payload.order_id);
|
||||
assert!(!payload.task_id.is_empty());
|
||||
}
|
||||
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
|
||||
use kinovi_web_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
|
||||
use kinovi_web_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
|
||||
use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
|
||||
use url_utils::extension::extract_extension_from_url::{extract_extension_from_url_str, ExtractExtensions};
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
@@ -8,14 +8,14 @@ use crate::errors::provider_error::ProviderError;
|
||||
use crate::utils::download_file::download_file;
|
||||
|
||||
/// Downloads an image from a source URL and re-uploads it to the
|
||||
/// KinoviWeb/Kinovi CDN. Returns the public URL.
|
||||
/// Seedance2Pro/Kinovi CDN. Returns the public URL.
|
||||
///
|
||||
/// This is the image-side twin of `generate_video::providers::kinovi::upload`.
|
||||
/// The actual upload path doesn't care whether the file is a video or an
|
||||
/// image, but keeping the two helpers separate lets each side evolve
|
||||
/// independently and makes intent clear at call sites.
|
||||
pub(crate) async fn upload_to_kinovi_web(
|
||||
session: &KinoviWebSession,
|
||||
pub(crate) async fn upload_to_seedance2pro(
|
||||
session: &Seedance2ProSession,
|
||||
source_url: &str,
|
||||
) -> Result<String, ArtcraftRouterError> {
|
||||
let extension = extract_extension_from_url_str(source_url, &ExtractExtensions::All)
|
||||
@@ -30,7 +30,7 @@ pub(crate) async fn upload_to_kinovi_web(
|
||||
host_override: None,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
let upload_response = upload_file(UploadFileArgs {
|
||||
upload_url: prepare_response.upload_url,
|
||||
@@ -38,7 +38,7 @@ pub(crate) async fn upload_to_kinovi_web(
|
||||
host_override: None,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(upload_response.public_url)
|
||||
}
|
||||
|
||||
+1
-1
@@ -268,7 +268,7 @@ mod tests {
|
||||
for provider in [
|
||||
RouterProvider::GmiCloud,
|
||||
RouterProvider::GrokApi,
|
||||
RouterProvider::KinoviWeb,
|
||||
RouterProvider::Seedance2Pro,
|
||||
RouterProvider::WorldLabs,
|
||||
] {
|
||||
let result = builder_with_url_image(provider, RouterMeshModel::Hunyuan3d3).build2();
|
||||
|
||||
+1
-1
@@ -252,7 +252,7 @@ mod tests {
|
||||
RouterProvider::Fal,
|
||||
RouterProvider::GmiCloud,
|
||||
RouterProvider::GrokApi,
|
||||
RouterProvider::KinoviWeb,
|
||||
RouterProvider::Seedance2Pro,
|
||||
] {
|
||||
let result = GenerateSplatRequestBuilder {
|
||||
provider,
|
||||
|
||||
@@ -74,17 +74,17 @@ generate_video/
|
||||
│ ├── seedance_2p0/ ← Seedance 2.0 Pro model (skips draft, returns Request directly)
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── build.rs ← builder → Request conversion + plan helpers
|
||||
│ │ ├── cost.rs ← standalone cost estimation (independent of kinovi_web_client)
|
||||
│ │ ├── cost.rs ← standalone cost estimation (independent of seedance2pro_client)
|
||||
│ │ └── request.rs ← request state + send() via Artcraft multi-function API
|
||||
│ └── seedance_2p0_fast/ ← Seedance 2.0 Fast model (skips draft, 480p/720p only)
|
||||
│ ├── mod.rs
|
||||
│ ├── build.rs ← builder → Request conversion + plan helpers (no 1080p)
|
||||
│ ├── cost.rs ← standalone cost estimation with Fast-specific pricing
|
||||
│ └── request.rs ← request state + send() via Artcraft omni-gen API
|
||||
└── kinovi/ ← Kinovi/KinoviWeb provider
|
||||
└── kinovi/ ← Kinovi/Seedance2Pro provider
|
||||
├── mod.rs
|
||||
├── resolve.rs ← shared: media token resolution, upload helpers
|
||||
├── upload.rs ← shared: download + re-upload to KinoviWeb CDN
|
||||
├── upload.rs ← shared: download + re-upload to Seedance2Pro CDN
|
||||
├── seedance_2p0/ ← Seedance 2.0 Pro model
|
||||
│ ├── mod.rs
|
||||
│ ├── build.rs ← builder → draft conversion + plan helpers
|
||||
|
||||
+6
-22
@@ -43,9 +43,6 @@ use crate::generate::generate_video::providers::artcraft::veo_3_fast::build::bui
|
||||
use crate::generate::generate_video::providers::artcraft::veo_3p1::build::build_artcraft_veo_3p1;
|
||||
use crate::generate::generate_video::providers::artcraft::veo_3p1_fast::build::build_artcraft_veo_3p1_fast;
|
||||
use crate::generate::generate_video::providers::artcraft::veo_3p1_lite::build::build_artcraft_veo_3p1_lite;
|
||||
use crate::generate::generate_video::providers::artcraft::flux_3::build::build_artcraft_flux_3;
|
||||
use crate::generate::generate_video::providers::artcraft::flux_3_draft::build::build_artcraft_flux_3_draft;
|
||||
use crate::generate::generate_video::providers::artcraft::minimax_h3::build::build_artcraft_minimax_h3;
|
||||
use crate::generate::generate_video::providers::artcraft::vidu_q3::build::build_artcraft_vidu_q3;
|
||||
use crate::generate::generate_video::providers::artcraft::vidu_q3_turbo::build::build_artcraft_vidu_q3_turbo;
|
||||
use crate::generate::generate_video::providers::kinovi::happy_horse_1p0::build::build_kinovi_happy_horse_1p0;
|
||||
@@ -71,15 +68,10 @@ use crate::generate::generate_video::providers::fal::veo_3_fast::build::build_fa
|
||||
use crate::generate::generate_video::providers::fal::veo_3p1::build::build_fal_veo_3p1;
|
||||
use crate::generate::generate_video::providers::fal::veo_3p1_fast::build::build_fal_veo_3p1_fast;
|
||||
use crate::generate::generate_video::providers::fal::veo_3p1_lite::build::build_fal_veo_3p1_lite;
|
||||
use crate::generate::generate_video::providers::fal::flux_3::build::build_fal_flux_3;
|
||||
use crate::generate::generate_video::providers::fal::flux_3_draft::build::build_fal_flux_3_draft;
|
||||
use crate::generate::generate_video::providers::fal::minimax_h3::build::build_fal_minimax_h3;
|
||||
use crate::generate::generate_video::providers::fal::vidu_q3::build::build_fal_vidu_q3;
|
||||
use crate::generate::generate_video::providers::fal::vidu_q3_turbo::build::build_fal_vidu_q3_turbo;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::build::build_kinovi_seedance_2p0_mini;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p5_preview::build::build_artcraft_seedance_2p5_preview;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p5_preview::build::build_kinovi_seedance_2p5_preview;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
/// RouterProvider-agnostic video generation request. Distilled by `build2()` into a
|
||||
@@ -178,8 +170,6 @@ impl GenerateVideoRequestBuilder {
|
||||
// Artcraft
|
||||
(RouterProvider::Artcraft, RouterVideoModel::GrokImagineVideo) => build_artcraft_grok_imagine_video(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::GrokImagineVideo1p5) => build_artcraft_grok_imagine_video_1p5(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Flux3) => build_artcraft_flux_3(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Flux3Draft) => build_artcraft_flux_3_draft(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::HappyHorse1p0) => build_artcraft_happy_horse_1p0(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Kling16Pro) => build_artcraft_kling_1_6_pro(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Kling21Master) => build_artcraft_kling_2_1_master(self),
|
||||
@@ -188,7 +178,6 @@ impl GenerateVideoRequestBuilder {
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Kling2p6Pro) => build_artcraft_kling_2_6_pro(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Kling3p0Pro) => build_artcraft_kling_3p0_pro(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Kling3p0Standard) => build_artcraft_kling_3p0_standard(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::MinimaxH3) => build_artcraft_minimax_h3(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0) => build_artcraft_seedance_2p0(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0Fast) => build_artcraft_seedance_2p0_fast(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0Ultra) => build_artcraft_seedance_2p0_u(self),
|
||||
@@ -200,7 +189,6 @@ impl GenerateVideoRequestBuilder {
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0Mini) => build_artcraft_seedance_2p0_mini(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusMini) => build_artcraft_seedance_2p0_bp_mini(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusUltraMini) => build_artcraft_seedance_2p0_bpu_mini(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p5Preview) => build_artcraft_seedance_2p5_preview(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::PreviewModel) => build_artcraft_preview_model(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::PreviewModelFast) => build_artcraft_preview_model_fast(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance10Lite) => build_artcraft_seedance_1p0_lite(self),
|
||||
@@ -216,8 +204,6 @@ impl GenerateVideoRequestBuilder {
|
||||
(RouterProvider::Artcraft, RouterVideoModel::ViduQ3) => build_artcraft_vidu_q3(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::ViduQ3Turbo) => build_artcraft_vidu_q3_turbo(self),
|
||||
// Fal
|
||||
(RouterProvider::Fal, RouterVideoModel::Flux3) => build_fal_flux_3(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Flux3Draft) => build_fal_flux_3_draft(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Kling16Pro) => build_fal_kling_1_6_pro(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Kling21Master) => build_fal_kling_2_1_master(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Kling21Pro) => build_fal_kling_2_1_pro(self),
|
||||
@@ -225,7 +211,6 @@ impl GenerateVideoRequestBuilder {
|
||||
(RouterProvider::Fal, RouterVideoModel::Kling2p6Pro) => build_fal_kling_2_6_pro(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Kling3p0Pro) => build_fal_kling_3p0_pro(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Kling3p0Standard) => build_fal_kling_3p0_standard(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::MinimaxH3) => build_fal_minimax_h3(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Seedance10Lite) => build_fal_seedance_1p0_lite(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Seedance1p5Pro) => build_fal_seedance_1p5_pro(self),
|
||||
(RouterProvider::Fal, RouterVideoModel::Sora2) => build_fal_sora_2(self),
|
||||
@@ -245,13 +230,12 @@ impl GenerateVideoRequestBuilder {
|
||||
(RouterProvider::GrokApi, RouterVideoModel::GrokImagineVideo) => build_grok_api_grok_imagine_video(self),
|
||||
(RouterProvider::GrokApi, RouterVideoModel::GrokImagineVideo1p5) => build_grok_api_grok_imagine_video_1p5(self),
|
||||
// Kinovi
|
||||
(RouterProvider::KinoviWeb, RouterVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self),
|
||||
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self),
|
||||
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0Fast) => build_kinovi_seedance_2p0_fast(self),
|
||||
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0Mini) => build_kinovi_seedance_2p0_mini(self),
|
||||
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0BytePlusMini) => build_kinovi_seedance_2p0_mini(self),
|
||||
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0BytePlusUltraMini) => build_kinovi_seedance_2p0_mini(self),
|
||||
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p5Preview) => build_kinovi_seedance_2p5_preview(self),
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self),
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self),
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0Fast) => build_kinovi_seedance_2p0_fast(self),
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0Mini) => build_kinovi_seedance_2p0_mini(self),
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0BytePlusMini) => build_kinovi_seedance_2p0_mini(self),
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0BytePlusUltraMini) => build_kinovi_seedance_2p0_mini(self),
|
||||
_ => self.unsupported_provider_and_model(),
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -10,7 +10,7 @@ pub struct ArtcraftVideoResponsePayload {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KinoviWebVideoResponsePayload {
|
||||
pub struct Seedance2proVideoResponsePayload {
|
||||
pub order_id: String,
|
||||
pub task_id: String,
|
||||
pub maybe_order_ids: Option<Vec<String>>,
|
||||
@@ -58,7 +58,7 @@ pub enum GenerateVideoResponse {
|
||||
Artcraft(ArtcraftVideoResponsePayload),
|
||||
GmiCloud(GmiCloudVideoResponsePayload),
|
||||
Grok(GrokVideoResponsePayload),
|
||||
KinoviWeb(KinoviWebVideoResponsePayload),
|
||||
Seedance2Pro(Seedance2proVideoResponsePayload),
|
||||
Fal(FalVideoResponsePayload),
|
||||
}
|
||||
|
||||
@@ -70,9 +70,9 @@ impl GenerateVideoResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_kinovi_web_payload(&self) -> Option<KinoviWebVideoResponsePayload> {
|
||||
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proVideoResponsePayload> {
|
||||
match self {
|
||||
Self::KinoviWeb(p) => Some(p.clone()),
|
||||
Self::Seedance2Pro(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video::providers::artcraft::flux_3::request::ArtcraftFlux3RequestState;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_flux_3(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let state = build_artcraft_flux_3_state(builder)?;
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftFlux3(state)))
|
||||
}
|
||||
|
||||
pub(crate) fn build_artcraft_flux_3_state(mut builder: GenerateVideoRequestBuilder) -> Result<ArtcraftFlux3RequestState, ArtcraftRouterError> {
|
||||
// Flux 3 accepts 5–20 seconds — wider than the generic helper's window —
|
||||
// so pre-plan the duration and reapply it after (the helper would clamp
|
||||
// 16–20s down). Also preserve generate_audio (the helper hardcodes None on
|
||||
// its output).
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
let final_duration = plan_flux_3_duration(builder.duration_seconds, strategy)?;
|
||||
builder.duration_seconds = final_duration;
|
||||
|
||||
let generate_audio = builder.generate_audio;
|
||||
let mut request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Flux3,
|
||||
SupportedResolutions::Full,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
request.generate_audio = generate_audio;
|
||||
request.duration_seconds = final_duration;
|
||||
|
||||
Ok(ArtcraftFlux3RequestState { request })
|
||||
}
|
||||
|
||||
/// Flux 3 durations: 5–20 seconds (unset lets the model pick).
|
||||
pub(crate) fn plan_flux_3_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u16>, ArtcraftRouterError> {
|
||||
const MIN: u16 = 5;
|
||||
const MAX: u16 = 20;
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(d) if (MIN..=MAX).contains(&d) => Ok(Some(d)),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", other),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(other.clamp(MIN, MAX)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
|
||||
use crate::generate::generate_video::providers::artcraft::flux_3::request::ArtcraftFlux3RequestState;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
/// Per-second rates in hundredths of a US cent.
|
||||
const LOW_RES_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 1_955;
|
||||
const HIGH_RES_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 3_335;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFlux3CostState {
|
||||
pub duration_seconds: u64,
|
||||
pub is_1080p: bool,
|
||||
}
|
||||
|
||||
impl ArtcraftFlux3CostState {
|
||||
pub fn from_request(request: &ArtcraftFlux3RequestState) -> Self {
|
||||
Self {
|
||||
// Flux 3 defaults None → 5s (the shortest selectable duration).
|
||||
duration_seconds: request.request.duration_seconds.map(u64::from).unwrap_or(5),
|
||||
is_1080p: is_1080p(request.request.resolution),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
let rate = if self.is_1080p {
|
||||
HIGH_RES_RATE_HUNDREDTH_CENTS_PER_SEC
|
||||
} else {
|
||||
LOW_RES_RATE_HUNDREDTH_CENTS_PER_SEC
|
||||
};
|
||||
// Round up to the next whole cent.
|
||||
let cost_in_usd_cents = (rate * self.duration_seconds).div_ceil(100);
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Flux 3 renders 720p or 1080p; 1080p and above land on the higher tier,
|
||||
/// while 720p and below render at 720p. The default (unset) resolution is
|
||||
/// 720p.
|
||||
fn is_1080p(resolution: Option<CommonResolutionEnum>) -> bool {
|
||||
match resolution {
|
||||
None => false, // defaults to 720p
|
||||
Some(CommonResolutionEnum::TenEightyP)
|
||||
| Some(CommonResolutionEnum::OneK)
|
||||
| Some(CommonResolutionEnum::TwoK)
|
||||
| Some(CommonResolutionEnum::ThreeK)
|
||||
| Some(CommonResolutionEnum::FourK) => true,
|
||||
Some(CommonResolutionEnum::HalfK)
|
||||
| Some(CommonResolutionEnum::FourEightyP)
|
||||
| Some(CommonResolutionEnum::SevenTwentyP) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::artcraft::flux_3::build::build_artcraft_flux_3_state;
|
||||
use crate::generate::generate_video::providers::artcraft::flux_3::cost::ArtcraftFlux3CostState;
|
||||
|
||||
#[test]
|
||||
fn default_settings_5s_is_98() { assert_eq!(cost_cents(Some(5), None), 98); }
|
||||
|
||||
#[test]
|
||||
fn default_duration_is_5s() { assert_eq!(cost_cents(None, None), 98); }
|
||||
|
||||
#[test]
|
||||
fn low_res_20s_is_391() { assert_eq!(cost_cents(Some(20), Some(RouterResolution::SevenTwentyP)), 391); }
|
||||
|
||||
#[test]
|
||||
fn four_eighty_p_lands_on_low_tier() {
|
||||
assert_eq!(cost_cents(Some(5), Some(RouterResolution::FourEightyP)), 98);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_res_5s_is_167() { assert_eq!(cost_cents(Some(5), Some(RouterResolution::TenEightyP)), 167); }
|
||||
|
||||
#[test]
|
||||
fn high_res_10s_is_334() { assert_eq!(cost_cents(Some(10), Some(RouterResolution::TenEightyP)), 334); }
|
||||
|
||||
#[test]
|
||||
fn odd_duration_rounds_up_to_whole_cents() {
|
||||
// 7s low res: 1955 × 7 = 13685 hundredth-cents → 137 cents.
|
||||
assert_eq!(cost_cents(Some(7), None), 137);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_classifier_defaults_720p() {
|
||||
assert!(!super::is_1080p(None));
|
||||
assert!(super::is_1080p(Some(CommonResolutionEnum::TenEightyP)));
|
||||
assert!(!super::is_1080p(Some(CommonResolutionEnum::SevenTwentyP)));
|
||||
assert!(!super::is_1080p(Some(CommonResolutionEnum::FourEightyP)));
|
||||
}
|
||||
|
||||
fn cost_cents(
|
||||
duration_seconds: Option<u16>,
|
||||
resolution: Option<RouterResolution>,
|
||||
) -> u64 {
|
||||
let b = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Flux3,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("test".to_string()),
|
||||
duration_seconds,
|
||||
resolution,
|
||||
..Default::default()
|
||||
};
|
||||
let state = build_artcraft_flux_3_state(b).unwrap();
|
||||
ArtcraftFlux3CostState::from_request(&state).estimate_cost().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::providers::artcraft::request_common::send_artcraft_omni_video_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFlux3RequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftFlux3RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_video_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use test_data::web::image_media_tokens::JUNO_AT_LAKE_PRODUCTION_MEDIA_TOKEN;
|
||||
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // sends a real generation to the Artcraft backend, incurs cost
|
||||
async fn text_to_video() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("A red kite soaring over coastal cliffs, cinematic.".to_string()),
|
||||
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
|
||||
..artcraft_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // sends a real generation to the Artcraft backend, incurs cost
|
||||
async fn keyframe_start_frame() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("The dog stands up and walks toward the lake.".to_string()),
|
||||
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new(JUNO_AT_LAKE_PRODUCTION_MEDIA_TOKEN.to_string()))),
|
||||
..artcraft_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn artcraft_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Flux3,
|
||||
provider: RouterProvider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_artcraft_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
|
||||
.expect("Failed to read artcraft cookies");
|
||||
let cookies = cookies.trim().to_string();
|
||||
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
|
||||
.expect("Failed to parse cookies")
|
||||
.expect("No credentials found");
|
||||
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
|
||||
let client = get_artcraft_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
VideoGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Artcraft skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateVideoResponse::Artcraft(p) => {
|
||||
println!("inference_job_token={:?}", p.inference_job_token);
|
||||
println!("all_inference_job_tokens={:?}", p.all_inference_job_tokens);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video::providers::artcraft::flux_3::build::plan_flux_3_duration;
|
||||
use crate::generate::generate_video::providers::artcraft::flux_3_draft::request::ArtcraftFlux3DraftRequestState;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_flux_3_draft(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let state = build_artcraft_flux_3_draft_state(builder)?;
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftFlux3Draft(state)))
|
||||
}
|
||||
|
||||
pub(crate) fn build_artcraft_flux_3_draft_state(mut builder: GenerateVideoRequestBuilder) -> Result<ArtcraftFlux3DraftRequestState, ArtcraftRouterError> {
|
||||
// Same duration handling as full-quality Flux 3: 5–20 seconds is wider than
|
||||
// the generic helper's window, so pre-plan and reapply after. Also preserve
|
||||
// generate_audio (the helper hardcodes None on its output).
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
let final_duration = plan_flux_3_duration(builder.duration_seconds, strategy)?;
|
||||
builder.duration_seconds = final_duration;
|
||||
|
||||
let generate_audio = builder.generate_audio;
|
||||
let mut request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Flux3Draft,
|
||||
SupportedResolutions::Full,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
request.generate_audio = generate_audio;
|
||||
request.duration_seconds = final_duration;
|
||||
|
||||
Ok(ArtcraftFlux3DraftRequestState { request })
|
||||
}
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
use crate::generate::generate_video::providers::artcraft::flux_3_draft::request::ArtcraftFlux3DraftRequestState;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
/// Per-second rate in hundredths of a US cent. Drafts always render 720p, so
|
||||
/// resolution does not affect the bill.
|
||||
const GENERATION_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 690;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFlux3DraftCostState {
|
||||
pub duration_seconds: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftFlux3DraftCostState {
|
||||
pub fn from_request(request: &ArtcraftFlux3DraftRequestState) -> Self {
|
||||
Self {
|
||||
// Flux 3 Draft defaults None → 5s (the shortest selectable duration).
|
||||
duration_seconds: request.request.duration_seconds.map(u64::from).unwrap_or(5),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
// Round up to the next whole cent.
|
||||
let cost_in_usd_cents =
|
||||
(GENERATION_RATE_HUNDREDTH_CENTS_PER_SEC * self.duration_seconds).div_ceil(100);
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::artcraft::flux_3_draft::build::build_artcraft_flux_3_draft_state;
|
||||
use crate::generate::generate_video::providers::artcraft::flux_3_draft::cost::ArtcraftFlux3DraftCostState;
|
||||
|
||||
#[test]
|
||||
fn default_settings_5s_is_35() { assert_eq!(cost_cents(Some(5), None), 35); }
|
||||
|
||||
#[test]
|
||||
fn default_duration_is_5s() { assert_eq!(cost_cents(None, None), 35); }
|
||||
|
||||
#[test]
|
||||
fn twenty_seconds_is_138() { assert_eq!(cost_cents(Some(20), None), 138); }
|
||||
|
||||
/// Drafts always render 720p; a requested resolution does not change the
|
||||
/// bill.
|
||||
#[test]
|
||||
fn cost_ignores_resolution() {
|
||||
for resolution in [None, Some(RouterResolution::SevenTwentyP), Some(RouterResolution::TenEightyP)] {
|
||||
assert_eq!(cost_cents(Some(5), resolution), 35, "resolution={resolution:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_duration_rounds_up_to_whole_cents() {
|
||||
// 7s generation: 690 × 7 = 4830 hundredth-cents → 49 cents.
|
||||
assert_eq!(cost_cents(Some(7), None), 49);
|
||||
}
|
||||
|
||||
fn cost_cents(
|
||||
duration_seconds: Option<u16>,
|
||||
resolution: Option<RouterResolution>,
|
||||
) -> u64 {
|
||||
let b = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Flux3Draft,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("test".to_string()),
|
||||
duration_seconds,
|
||||
resolution,
|
||||
..Default::default()
|
||||
};
|
||||
let state = build_artcraft_flux_3_draft_state(b).unwrap();
|
||||
ArtcraftFlux3DraftCostState::from_request(&state).estimate_cost().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user