mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-30 05:22:51 -04:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1b8128036 | ||
|
|
bf1bc62346 | ||
|
|
1e681a5825 | ||
|
|
4fbf8625cc | ||
|
|
c4d063beb9 | ||
|
|
1bd8c46e51 | ||
|
|
288ba16ab4 | ||
|
|
e106b2179f | ||
|
|
fa9e95187f |
@@ -6,7 +6,6 @@
|
||||
.env-elasticsearch-secrets
|
||||
.env-migration-tool-secrets
|
||||
.env-secrets
|
||||
.env-support-tool-secrets
|
||||
_develop/tools/.secrets
|
||||
_develop/tools/db_secrets.toml
|
||||
crates/service/web/storyteller_web/config/storyteller-web.development-secrets.env
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n \n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 14
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "002cdb3d086f3b4181e2997befa54bd8516cb1d3d3bb6e389862695318d5c299"
|
||||
}
|
||||
+20
-31
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n jobs.failure_reason,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n jobs.on_success_result_batch_token as maybe_result_batch_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n jobs.on_success_result_batch_token as maybe_result_batch_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -135,16 +135,6 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "failure_reason",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 2048
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "maybe_result_entity_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -154,7 +144,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 14,
|
||||
"name": "maybe_result_entity_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -164,7 +154,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 15,
|
||||
"name": "maybe_result_batch_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -174,7 +164,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 16,
|
||||
"name": "maybe_model_weights_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -184,7 +174,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 17,
|
||||
"name": "maybe_tts_model_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -194,7 +184,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"ordinal": 18,
|
||||
"name": "maybe_voice_conversion_model_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -204,7 +194,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"ordinal": 19,
|
||||
"name": "maybe_tts_public_bucket_path",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -214,7 +204,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"ordinal": 20,
|
||||
"name": "maybe_voice_conversion_public_bucket_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -224,7 +214,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"ordinal": 21,
|
||||
"name": "maybe_media_file_public_bucket_directory_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -234,7 +224,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"ordinal": 22,
|
||||
"name": "maybe_media_file_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -244,7 +234,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"ordinal": 23,
|
||||
"name": "maybe_media_file_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -254,7 +244,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"ordinal": 24,
|
||||
"name": "maybe_assigned_worker",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -264,7 +254,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"ordinal": 25,
|
||||
"name": "maybe_assigned_cluster",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -274,7 +264,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"ordinal": 26,
|
||||
"name": "is_keepalive_required",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -284,7 +274,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"ordinal": 27,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -294,7 +284,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"ordinal": 28,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -304,7 +294,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"ordinal": 29,
|
||||
"name": "maybe_first_started_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -314,7 +304,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 31,
|
||||
"ordinal": 30,
|
||||
"name": "maybe_successfully_completed_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -324,7 +314,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 32,
|
||||
"ordinal": 31,
|
||||
"name": "database_clock",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
@@ -364,7 +354,6 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -373,5 +362,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a2c9e2f61eba666254f8432b3c50a0a7738ee3b3faa77c5281bc8e4af96159b9"
|
||||
"hash": "243afe0bc574f9f233c7797add9faf96440e2bafd52032e07951272fb1ec78b1"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 16
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "29916e73a1784ea0b70ef4f637520b05efccc31e01840f681e9b042f82eb8f7e"
|
||||
}
|
||||
+5
-17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n on_complete_primary_media_file_token,\n on_complete_primary_media_file_class,\n on_complete_batch_token,\n on_complete_primary_media_file_cdn_url,\n on_complete_primary_media_file_thumbnail_url_template,\n on_failure_type,\n on_failure_message,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
|
||||
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n on_complete_primary_media_file_token,\n on_complete_primary_media_file_class,\n on_complete_batch_token,\n on_complete_primary_media_file_cdn_url,\n on_complete_primary_media_file_thumbnail_url_template,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -73,29 +73,19 @@
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_failure_type",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_failure_message",
|
||||
"ordinal": 15,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"ordinal": 16,
|
||||
"ordinal": 14,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"ordinal": 17,
|
||||
"ordinal": 15,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "completed_at: DateTime<Utc>",
|
||||
"ordinal": 18,
|
||||
"ordinal": 16,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
@@ -117,12 +107,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "2436eb7bf4b0e23a899311a67a77f71d89eb35461e2113e43cd9d221bfa91c9a"
|
||||
"hash": "47f94c127abc20683b25c257aed3c2190b9bd29eb829d61392f08e10fd986767"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO wallets\nSET\n token = ?,\n wallet_namespace = ?,\n\n owner_user_token = ?,\n\n banked_credits = 0,\n monthly_credits = 0\n ",
|
||||
"query": "\nINSERT INTO wallets\nSET\n token = ?,\n wallet_namespace = ?,\n\n owner_user_token = ?,\n \n banked_credits = 0,\n monthly_credits = 0\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -8,5 +8,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f4fb945b4311bd4458a68d82e417b40f8c36c08d572c07881b89cd157a8a8699"
|
||||
"hash": "4f5100a2cfb465964e77eaacb1ae394daae94de20c56a0966a8222dc9bea63c6"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE tasks\n SET task_status = ?,\n on_failure_type = ?,\n on_failure_message = ?\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9b99ccd0baf9a4b74d8903f63e3a3dcf4c09b75cad978f0532388cbd743b7516"
|
||||
}
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_cover_image_media_file_token = ?,\n\n maybe_mod_user_token = ?,\n is_generated_on_prem = ?,\n generated_by_worker = ?,\n generated_by_cluster = ?\n ",
|
||||
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_mod_user_token = ?, \n is_generated_on_prem = ?, \n generated_by_worker = ?, \n generated_by_cluster = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 40
|
||||
"Right": 39
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "87d5511700985daaabfc9f2972f018d56b6672c07be082daa7c1a782400cb18d"
|
||||
"hash": "b4470e607c005421dd8504b9f1d695080a581936ac6ce00b6f437b2764e741be"
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n username,\n display_name,\n username_is_generated as `username_is_generated: bool`,\n username_is_not_customized as `username_is_not_customized: bool`\nFROM users\nWHERE\n token = ?\n AND user_deleted_at IS NULL\n AND mod_deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "username_is_generated: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "username_is_not_customized: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "be9867966f24f44e14f96966cf5113055f00c339dda75334e7e88d355b9bfe72"
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `operation_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "operation_id",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 256
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "creator_ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 160
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "creator_set_visibility: enums::common::visibility::Visibility",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | BINARY | ENUM",
|
||||
"char_set": 224,
|
||||
"max_size": 28
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d56a26a7300495fac9741e77e390f16402e2273dbab9a15d7c17eb1f0da7a1b1"
|
||||
}
|
||||
Generated
+20
-90
@@ -578,9 +578,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
@@ -2225,9 +2225,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.60"
|
||||
version = "4.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
||||
checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -2235,21 +2235,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.60"
|
||||
version = "4.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
||||
checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex 1.0.0",
|
||||
"clap_lex 0.7.4",
|
||||
"strsim 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.55"
|
||||
version = "4.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -2268,9 +2268,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.0.0"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "cloud_storage"
|
||||
@@ -3068,7 +3068,7 @@ version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap 4.5.60",
|
||||
"clap 4.5.13",
|
||||
"datetimes",
|
||||
"dotenv",
|
||||
"easyenv",
|
||||
@@ -3103,7 +3103,7 @@ name = "db-cleanup"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.60",
|
||||
"clap 4.5.13",
|
||||
"dotenv",
|
||||
"easyenv",
|
||||
"enums",
|
||||
@@ -3263,7 +3263,7 @@ name = "dev-database-seed"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.60",
|
||||
"clap 4.5.13",
|
||||
"cloud_storage",
|
||||
"config",
|
||||
"easyenv",
|
||||
@@ -3289,7 +3289,7 @@ name = "dev-upload-media-file"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.60",
|
||||
"clap 4.5.13",
|
||||
"cloud_storage",
|
||||
"config",
|
||||
"easyenv",
|
||||
@@ -3763,7 +3763,7 @@ name = "elasticsearch-cli"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.60",
|
||||
"clap 4.5.13",
|
||||
"config",
|
||||
"easyenv",
|
||||
"elasticsearch",
|
||||
@@ -3934,7 +3934,6 @@ dependencies = [
|
||||
name = "enums"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -6745,7 +6744,7 @@ dependencies = [
|
||||
name = "lru-delete-models"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"clap 4.5.60",
|
||||
"clap 4.5.13",
|
||||
"errors",
|
||||
"logging",
|
||||
"walkdir",
|
||||
@@ -7055,7 +7054,7 @@ name = "migration-tool"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.60",
|
||||
"clap 4.5.13",
|
||||
"cloud_storage",
|
||||
"config",
|
||||
"easyenv",
|
||||
@@ -7229,29 +7228,6 @@ dependencies = [
|
||||
"thiserror 1.0.63",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muapi_client"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"cloudflare_errors",
|
||||
"env",
|
||||
"env_logger 0.11.9",
|
||||
"errors",
|
||||
"log",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"test_data",
|
||||
"testing",
|
||||
"tokio",
|
||||
"url",
|
||||
"workspace-build-acceleration",
|
||||
"wreq",
|
||||
"wreq-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.17.1"
|
||||
@@ -10340,7 +10316,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
name = "scrape-test"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"clap 4.5.60",
|
||||
"clap 4.5.13",
|
||||
"errors",
|
||||
"tokio",
|
||||
"web_scrapers",
|
||||
@@ -11621,7 +11597,6 @@ dependencies = [
|
||||
"tts_common",
|
||||
"url",
|
||||
"url_config",
|
||||
"url_utils",
|
||||
"user_input_common",
|
||||
"user_traits_component",
|
||||
"users",
|
||||
@@ -11774,20 +11749,6 @@ dependencies = [
|
||||
"rsa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "support-tool"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.5.60",
|
||||
"easyenv",
|
||||
"errors",
|
||||
"log",
|
||||
"seedance2pro",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
@@ -14953,7 +14914,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"cc",
|
||||
"chrono",
|
||||
"clap 4.5.60",
|
||||
"clap 4.5.13",
|
||||
"cookie 0.18.1",
|
||||
"core-foundation-sys",
|
||||
"crossbeam-utils",
|
||||
@@ -15106,37 +15067,6 @@ dependencies = [
|
||||
"wreq-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "worldlabs-job"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"actix_helpers",
|
||||
"anyhow",
|
||||
"bootstrap",
|
||||
"bucket_paths",
|
||||
"cloud_storage",
|
||||
"concurrency",
|
||||
"config",
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
"hashing",
|
||||
"jobs_common",
|
||||
"log",
|
||||
"mysql_queries",
|
||||
"reqwest 0.12.23",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"server_environment",
|
||||
"sqlx",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"workspace-build-acceleration",
|
||||
"world_labs_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wreq"
|
||||
version = "6.0.0-rc.28"
|
||||
|
||||
@@ -16,7 +16,6 @@ members = [
|
||||
"crates/api_clients/midjourney_client",
|
||||
"crates/api_clients/openai_sora_client",
|
||||
"crates/api_clients/openart_client",
|
||||
"crates/api_clients/muapi_client",
|
||||
"crates/api_clients/seedance2pro",
|
||||
"crates/api_clients/runwayml_client",
|
||||
"crates/api_clients/thumbnail_generator",
|
||||
@@ -34,7 +33,6 @@ members = [
|
||||
"crates/cli/production/kubectl-pod-cleanup",
|
||||
"crates/cli/production/lru-delete-models",
|
||||
"crates/cli/production/migration-tool",
|
||||
"crates/cli/production/support-tool",
|
||||
|
||||
# Desktop apps
|
||||
"crates/desktop/artcraft",
|
||||
@@ -122,7 +120,6 @@ members = [
|
||||
"crates/service/job/es-update-job",
|
||||
"crates/service/job/inference_job",
|
||||
"crates/service/job/seedance2_pro_job",
|
||||
"crates/service/job/worldlabs_job",
|
||||
"crates/service/job/tts_download_job",
|
||||
"crates/service/job/tts_inference_job",
|
||||
"crates/service/job/w2l_download_job",
|
||||
@@ -224,7 +221,6 @@ component_traits = { path = "crates/service/plugins/component_traits" }
|
||||
cookie_store = { path = "crates/lib/cookie_store" }
|
||||
cookies = { path = "crates/lib/cookies" }
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
easyenv = { path = "crates/lib/easyenv" }
|
||||
enums = { path = "crates/schema/public/enums" }
|
||||
enums_public = { path = "crates/schema/public/enums_public" }
|
||||
errors = { path = "crates/lib/errors" }
|
||||
@@ -241,7 +237,6 @@ memory_store = { path = "crates/lib/caching/memory_store" }
|
||||
midjourney_client = { path = "crates/api_clients/midjourney_client" }
|
||||
mimetypes = { path = "crates/lib/files/mimetypes" }
|
||||
openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
|
||||
muapi_client = { path = "crates/api_clients/muapi_client" }
|
||||
seedance2pro = { path = "crates/api_clients/seedance2pro" }
|
||||
primitives = { path = "crates/lib/data/primitives" }
|
||||
shared_service_components = { path = "crates/lib/shared_service_components" }
|
||||
@@ -275,7 +270,6 @@ base64 = "0.22.1"
|
||||
bstr = "1.12.1"
|
||||
bytes = "1.10.1"
|
||||
chrono = { version = "0.4.41", features = ["serde"] }
|
||||
clap = { version = "4.5.60", features = ["derive"] }
|
||||
cookie = "0.18.1"
|
||||
env = "1.0.1"
|
||||
env_logger = "0.11.9"
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
-- on_complete_primary_media_file_class,
|
||||
-- on_complete_primary_media_file_cdn_url,
|
||||
-- on_complete_primary_media_file_thumbnail_url_template,
|
||||
-- tasks_v6.sqlite - added two fields:
|
||||
-- on_failure_type
|
||||
-- on_failure_message
|
||||
--
|
||||
|
||||
CREATE TABLE tasks (
|
||||
-- Task auto-incrementing primary key.
|
||||
@@ -86,15 +84,6 @@ CREATE TABLE tasks (
|
||||
-- The URL template for the thumbnail of the primary media file.
|
||||
on_complete_primary_media_file_thumbnail_url_template TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- An enum-like string describing the failure reason (even for non-terminal failures).
|
||||
-- The enum is `TaskFailureType`, which is a close analogue of the backend API's `FrontendFailureCategory`.
|
||||
on_failure_type TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- A human-readable message describing the failure reason (even for non-terminal failures).
|
||||
on_failure_message TEXT,
|
||||
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||
completed_at INTEGER DEFAULT NULL
|
||||
|
||||
@@ -143,12 +143,6 @@ RUN SQLX_OFFLINE=true \
|
||||
--release \
|
||||
--bin seedance2-pro-job
|
||||
|
||||
RUN SQLX_OFFLINE=true \
|
||||
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
|
||||
$HOME/.cargo/bin/cargo build \
|
||||
--release \
|
||||
--bin worldlabs-job
|
||||
|
||||
RUN SQLX_OFFLINE=true \
|
||||
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
|
||||
$HOME/.cargo/bin/cargo build \
|
||||
@@ -209,7 +203,6 @@ RUN echo -n ${GIT_SHA} > GIT_SHA
|
||||
# Copy all the binaries (except those that need a GPU):
|
||||
COPY --from=builder /tmp/target/release/storyteller-web /
|
||||
COPY --from=builder /tmp/target/release/seedance2-pro-job /
|
||||
COPY --from=builder /tmp/target/release/worldlabs-job /
|
||||
COPY --from=builder /tmp/target/release/dummy-service /
|
||||
COPY --from=builder /tmp/target/release/analytics-job /
|
||||
COPY --from=builder /tmp/target/release/email-sender-job /
|
||||
|
||||
-6
@@ -23,12 +23,6 @@ pub struct Seedance2p0MultiFunctionVideoGenRequest {
|
||||
/// Optional reference images (reference mode). When present, takes priority over start/end frames.
|
||||
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Optional reference videos.
|
||||
pub reference_video_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Optional reference audio files.
|
||||
pub reference_audio_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Video aspect ratio / resolution.
|
||||
pub aspect_ratio: Option<Seedance2p0AspectRatio>,
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_api_clients::FrontendFailureCategoryForApiClients;
|
||||
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_old_clients::FrontendFailureCategoryForOldClients;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
|
||||
pub const LIST_SESSION_JOBS_URL_PATH: &str = "/v1/jobs/session";
|
||||
@@ -100,19 +98,9 @@ pub struct ListSessionStatusDetailsResponse {
|
||||
/// This is typically only for non-premium users.
|
||||
pub requires_keepalive: bool,
|
||||
|
||||
/// NB: DO NOT CHANGE: Old clients can't read the new enum payload values as they are introduced.
|
||||
///
|
||||
/// An enum the frontend can use to display localized/I18N error
|
||||
/// messages. These pertain to both transient and permanent failures.
|
||||
#[deprecated(note="use maybe_failure_category_updated")]
|
||||
pub maybe_failure_category: Option<FrontendFailureCategoryForOldClients>,
|
||||
|
||||
/// An enum the frontend can use to display localized/I18N error
|
||||
/// messages. These pertain to both transient and permanent failures.
|
||||
pub maybe_failure_category_updated: Option<FrontendFailureCategoryForApiClients>,
|
||||
|
||||
/// User-facing failure messages
|
||||
pub maybe_failure_message: Option<String>,
|
||||
pub maybe_failure_category: Option<FrontendFailureCategory>,
|
||||
|
||||
/// This is an integer number between 0 and 100 (both inclusive) that
|
||||
/// reports the completeness.
|
||||
@@ -137,3 +125,4 @@ pub struct ListSessionResultDetailsResponse {
|
||||
|
||||
pub maybe_successfully_completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod list_user_wallets;
|
||||
pub mod moderator_add_banked_balance_to_wallet;
|
||||
pub mod moderator_create_wallet_for_user;
|
||||
pub mod moderator_get_wallet;
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
use enums::common::payments_namespace::PaymentsNamespace;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::users::UserToken;
|
||||
use tokens::tokens::wallets::WalletToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const MODERATOR_CREATE_WALLET_FOR_USER_PATH: &str = "/v1/moderation/wallet/create_for_user";
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ModeratorCreateWalletForUserRequest {
|
||||
pub user_token: Option<UserToken>,
|
||||
pub payments_namespace: Option<PaymentsNamespace>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModeratorCreateWalletForUserResponse {
|
||||
pub success: bool,
|
||||
pub wallet_token: WalletToken,
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum AudioListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod audio_list_ref;
|
||||
pub mod common_aspect_ratio;
|
||||
pub mod common_image_model;
|
||||
pub mod common_resolution;
|
||||
@@ -7,4 +6,3 @@ pub mod common_video_model;
|
||||
pub mod image_list_ref;
|
||||
pub mod image_ref;
|
||||
pub mod provider;
|
||||
pub mod video_list_ref;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum VideoListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
}
|
||||
-2
@@ -69,8 +69,6 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
-2
@@ -69,8 +69,6 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
-2
@@ -72,8 +72,6 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
-5
@@ -31,9 +31,6 @@ pub (crate) fn estimate_video_cost_artcraft_seedance2p0(
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
use_face_blur_hack: None,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = args.estimate_cost_in_usd_cents();
|
||||
@@ -64,8 +61,6 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
-2
@@ -18,8 +18,6 @@ pub async fn execute_artcraft_seedance2p0(
|
||||
start_frame_media_token: plan.start_frame.map(|t| t.to_owned()),
|
||||
end_frame_media_token: plan.end_frame.map(|t| t.to_owned()),
|
||||
reference_image_media_tokens: plan.reference_images.map(|tokens| tokens.to_owned()),
|
||||
reference_video_media_tokens: plan.reference_videos.map(|tokens| tokens.to_owned()),
|
||||
reference_audio_media_tokens: plan.reference_audio.map(|tokens| tokens.to_owned()),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
duration_seconds: plan.duration_seconds,
|
||||
batch_count: Some(plan.batch_count),
|
||||
|
||||
-8
@@ -1,11 +1,9 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_pro::plan_generate_video_artcraft_kling3p0_pro;
|
||||
@@ -36,12 +34,6 @@ pub struct GenerateVideoRequest<'a> {
|
||||
/// Reference images (optional).
|
||||
pub reference_images: Option<ImageListRef<'a>>,
|
||||
|
||||
/// Reference videos (optional).
|
||||
pub reference_videos: Option<VideoListRef<'a>>,
|
||||
|
||||
/// Reference audio (optional).
|
||||
pub reference_audio: Option<AudioListRef<'a>>,
|
||||
|
||||
/// The resolution to use
|
||||
pub resolution: Option<CommonResolution>,
|
||||
|
||||
|
||||
-2
@@ -177,8 +177,6 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
|
||||
-2
@@ -177,8 +177,6 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
|
||||
-2
@@ -190,8 +190,6 @@ mod tests {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
|
||||
-32
@@ -1,8 +1,6 @@
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
@@ -18,8 +16,6 @@ pub struct PlanArtcraftSeedance2p0<'a> {
|
||||
pub start_frame: Option<&'a MediaFileToken>,
|
||||
pub end_frame: Option<&'a MediaFileToken>,
|
||||
pub reference_images: Option<&'a Vec<MediaFileToken>>,
|
||||
pub reference_videos: Option<&'a Vec<MediaFileToken>>,
|
||||
pub reference_audio: Option<&'a Vec<MediaFileToken>>,
|
||||
pub aspect_ratio: Option<Seedance2p0AspectRatio>,
|
||||
pub duration_seconds: Option<u8>,
|
||||
pub batch_count: Seedance2p0BatchCount,
|
||||
@@ -34,8 +30,6 @@ pub fn plan_generate_video_artcraft_seedance2p0<'a>(
|
||||
let start_frame = resolve_image_ref(request.start_frame)?;
|
||||
let end_frame = resolve_image_ref(request.end_frame)?;
|
||||
let reference_images = resolve_image_list_ref(request.reference_images)?;
|
||||
let reference_videos = resolve_video_list_ref(request.reference_videos)?;
|
||||
let reference_audio = resolve_audio_list_ref(request.reference_audio)?;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
|
||||
@@ -46,8 +40,6 @@ pub fn plan_generate_video_artcraft_seedance2p0<'a>(
|
||||
start_frame,
|
||||
end_frame,
|
||||
reference_images,
|
||||
reference_videos,
|
||||
reference_audio,
|
||||
aspect_ratio,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
@@ -79,30 +71,6 @@ fn resolve_image_list_ref<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_video_list_ref<'a>(
|
||||
video_list_ref: Option<VideoListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match video_list_ref {
|
||||
None => Ok(None),
|
||||
Some(VideoListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(VideoListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_audio_list_ref<'a>(
|
||||
audio_list_ref: Option<AudioListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match audio_list_ref {
|
||||
None => Ok(None),
|
||||
Some(AudioListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(AudioListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Supported aspect ratios and their AR values (width / height):
|
||||
// Portrait9x16 = 0.5625, Portrait3x4 = 0.75, Square1x1 = 1.0, Standard4x3 = 1.33, Landscape16x9 = 1.78
|
||||
//
|
||||
|
||||
@@ -290,8 +290,6 @@ pub fn base_video_request() -> GenerateVideoRequest<'static> {
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
[package]
|
||||
name = "muapi_client"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
name = "muapi_client"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
workspace-build-acceleration.workspace = true
|
||||
errors.workspace = true
|
||||
chrono.workspace = true
|
||||
cloudflare_errors.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
wreq-util.workspace = true
|
||||
wreq.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
env.workspace = true
|
||||
env_logger.workspace = true
|
||||
test_data = { path = "../../lib/test_data" }
|
||||
testing = { path = "../../lib/testing" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
@@ -1 +0,0 @@
|
||||
pub mod request_id;
|
||||
@@ -1,24 +0,0 @@
|
||||
/// ID for a prediction request in the Muapi API.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RequestId(String);
|
||||
|
||||
impl RequestId {
|
||||
|
||||
pub fn new(value: String) -> Self {
|
||||
RequestId(value)
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Self {
|
||||
RequestId(value.to_string())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RequestId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod muapi_api_key;
|
||||
pub mod muapi_session;
|
||||
@@ -1,20 +0,0 @@
|
||||
/// An API key is the credential required to interact with the Muapi API.
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MuapiApiKey {
|
||||
api_key: String,
|
||||
}
|
||||
|
||||
impl MuapiApiKey {
|
||||
pub fn new(api_key: String) -> Self {
|
||||
MuapiApiKey { api_key }
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.api_key
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
self.api_key.clone()
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
use crate::creds::muapi_api_key::MuapiApiKey;
|
||||
|
||||
/// Holds the full session data needed to make authenticated requests to Muapi.
|
||||
#[derive(Clone)]
|
||||
pub struct MuapiSession {
|
||||
pub api_key: MuapiApiKey,
|
||||
}
|
||||
|
||||
impl MuapiSession {
|
||||
pub fn new(api_key: MuapiApiKey) -> Self {
|
||||
MuapiSession { api_key }
|
||||
}
|
||||
|
||||
pub fn from_api_key_string(api_key: String) -> Self {
|
||||
MuapiSession {
|
||||
api_key: MuapiApiKey::new(api_key),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod muapi_client_error;
|
||||
pub mod muapi_error;
|
||||
pub mod muapi_generic_api_error;
|
||||
pub mod muapi_specific_api_error;
|
||||
@@ -1,26 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MuapiClientError {
|
||||
/// No API key is present in the session.
|
||||
NoApiKeyPresent,
|
||||
|
||||
/// Error parsing a URL.
|
||||
UrlParseError(url::ParseError),
|
||||
|
||||
/// An error was encountered in building the Wreq client.
|
||||
WreqClientError(wreq::Error),
|
||||
}
|
||||
|
||||
impl Error for MuapiClientError {}
|
||||
|
||||
impl Display for MuapiClientError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoApiKeyPresent => write!(f, "No API key present in the session."),
|
||||
Self::UrlParseError(err) => write!(f, "URL parse error: {}", err),
|
||||
Self::WreqClientError(err) => write!(f, "Wreq client error (during client creation): {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
use crate::error::muapi_client_error::MuapiClientError;
|
||||
use crate::error::muapi_generic_api_error::MuapiGenericApiError;
|
||||
use crate::error::muapi_specific_api_error::MuapiSpecificApiError;
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MuapiError {
|
||||
Client(MuapiClientError),
|
||||
ApiSpecific(MuapiSpecificApiError),
|
||||
ApiGeneric(MuapiGenericApiError),
|
||||
}
|
||||
|
||||
impl MuapiError {
|
||||
pub fn is_having_downtime_issues(&self) -> bool {
|
||||
match self {
|
||||
Self::ApiGeneric(MuapiGenericApiError::CloudflareError(CloudflareError::BadGateway502)) => true,
|
||||
Self::ApiGeneric(MuapiGenericApiError::CloudflareError(CloudflareError::GatewayTimeout504)) => true,
|
||||
Self::ApiGeneric(MuapiGenericApiError::CloudflareError(CloudflareError::TimeoutOccurred524)) => true,
|
||||
Self::ApiGeneric(MuapiGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body: _ }) => {
|
||||
match status_code.as_u16() {
|
||||
502 => true,
|
||||
504 => true,
|
||||
524 => true,
|
||||
_ => false,
|
||||
}
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for MuapiError {}
|
||||
|
||||
impl std::fmt::Display for MuapiError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Client(e) => write!(f, "MuapiClientError: {:?}", e),
|
||||
Self::ApiSpecific(e) => write!(f, "MuapiSpecificApiError: {:?}", e),
|
||||
Self::ApiGeneric(e) => write!(f, "MuapiGenericApiError: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MuapiClientError> for MuapiError {
|
||||
fn from(error: MuapiClientError) -> Self {
|
||||
Self::Client(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MuapiSpecificApiError> for MuapiError {
|
||||
fn from(error: MuapiSpecificApiError) -> Self {
|
||||
Self::ApiSpecific(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MuapiGenericApiError> for MuapiError {
|
||||
fn from(error: MuapiGenericApiError) -> Self {
|
||||
Self::ApiGeneric(error)
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use wreq::StatusCode;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MuapiGenericApiError {
|
||||
/// Specific Cloudflare errors.
|
||||
CloudflareError(CloudflareError),
|
||||
|
||||
/// serde_json::Error, likely from JSON deserialization schema mismatch.
|
||||
/// Includes the original body.
|
||||
SerdeResponseParseErrorWithBody(serde_json::Error, String),
|
||||
|
||||
/// serde_json::Error on a non-200 (error) response.
|
||||
/// Includes the original body.
|
||||
SerdeParseErrorWithBodyOnNon200(serde_json::Error, String),
|
||||
|
||||
/// An uncategorized bad HTTP response with status code and body.
|
||||
UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
},
|
||||
|
||||
/// An uncaught error from the HTTP client.
|
||||
WreqError(wreq::Error),
|
||||
}
|
||||
|
||||
impl Error for MuapiGenericApiError {}
|
||||
|
||||
impl Display for MuapiGenericApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CloudflareError(err) => write!(f, "Cloudflare error: {}", err),
|
||||
Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body),
|
||||
Self::SerdeParseErrorWithBodyOnNon200(err, body) => write!(f, "Failed to parse non-200 response body: {:?}. Body: {}", err, body),
|
||||
Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body),
|
||||
Self::WreqError(err) => write!(f, "Wreq client error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MuapiSpecificApiError {
|
||||
/// Invalid API key.
|
||||
InvalidApiKey,
|
||||
}
|
||||
|
||||
impl Error for MuapiSpecificApiError {}
|
||||
|
||||
impl Display for MuapiSpecificApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidApiKey => write!(f, "Unauthorized: invalid API key."),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#[cfg(test)]
|
||||
pub (crate) mod test_utils;
|
||||
|
||||
pub mod api_types;
|
||||
pub mod creds;
|
||||
pub mod error;
|
||||
pub mod requests;
|
||||
pub mod utils;
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod poll_prediction_result;
|
||||
pub mod seedance_2p0_image_to_video;
|
||||
pub mod seedance_2p0_text_to_video;
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod poll_prediction_result;
|
||||
mod request_types;
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
use crate::api_types::request_id::RequestId;
|
||||
use crate::creds::muapi_session::MuapiSession;
|
||||
use crate::error::muapi_client_error::MuapiClientError;
|
||||
use crate::error::muapi_error::MuapiError;
|
||||
use crate::error::muapi_generic_api_error::MuapiGenericApiError;
|
||||
use crate::requests::poll_prediction_result::request_types::*;
|
||||
use log::info;
|
||||
use wreq::Client;
|
||||
|
||||
const PREDICTIONS_BASE_URL: &str = "https://api.muapi.ai/api/v1/predictions";
|
||||
|
||||
// --- Public types ---
|
||||
|
||||
/// The lifecycle status of a prediction.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum PredictionStatus {
|
||||
/// The task is queued and has not started yet.
|
||||
Pending,
|
||||
/// The task is actively being processed.
|
||||
Processing,
|
||||
/// The task finished successfully. `outputs` will be populated.
|
||||
Completed,
|
||||
/// The task failed. `error` will contain the reason.
|
||||
Failed,
|
||||
/// An unrecognised status string was returned by the server.
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl PredictionStatus {
|
||||
fn from_str(s: &str) -> Self {
|
||||
match s {
|
||||
"pending" => Self::Pending,
|
||||
"processing" => Self::Processing,
|
||||
"completed" => Self::Completed,
|
||||
"failed" => Self::Failed,
|
||||
other => Self::Unknown(other.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_terminal(&self) -> bool {
|
||||
matches!(self, Self::Completed | Self::Failed)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Args & response ---
|
||||
|
||||
pub struct PollPredictionResultArgs<'a> {
|
||||
pub session: &'a MuapiSession,
|
||||
|
||||
/// The request_id returned from a submission endpoint.
|
||||
pub request_id: &'a RequestId,
|
||||
}
|
||||
|
||||
pub struct PollPredictionResultApiResponse {
|
||||
/// The current status of the prediction.
|
||||
pub status: PredictionStatus,
|
||||
|
||||
/// Output URLs. Populated when `status` is `Completed`.
|
||||
pub output_urls: Option<Vec<String>>,
|
||||
|
||||
/// Error message. Populated when `status` is `Failed`.
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
// --- Implementation ---
|
||||
|
||||
pub async fn poll_prediction_result(
|
||||
args: PollPredictionResultArgs<'_>,
|
||||
) -> Result<PollPredictionResultApiResponse, MuapiError> {
|
||||
let url = format!("{}/{}/result", PREDICTIONS_BASE_URL, args.request_id.as_str());
|
||||
|
||||
info!("Polling Muapi prediction result: {}", url);
|
||||
|
||||
let api_key = args.session.api_key.as_str();
|
||||
|
||||
let client = Client::builder()
|
||||
.build()
|
||||
.map_err(|err| MuapiClientError::WreqClientError(err))?;
|
||||
|
||||
let response = client.get(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("x-api-key", api_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| MuapiGenericApiError::WreqError(err))?;
|
||||
|
||||
let status = response.status();
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| MuapiGenericApiError::WreqError(err))?;
|
||||
|
||||
info!("Muapi poll response status: {}, body: {}", status, response_body);
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(MuapiGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: status,
|
||||
body: response_body,
|
||||
}.into());
|
||||
}
|
||||
|
||||
let parsed: PollPredictionResultResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| MuapiGenericApiError::SerdeResponseParseErrorWithBody(err, response_body))?;
|
||||
|
||||
let output_urls = parsed.outputs.and_then(|urls| if urls.is_empty() { None } else { Some(urls) });
|
||||
|
||||
Ok(PollPredictionResultApiResponse {
|
||||
status: PredictionStatus::from_str(&parsed.status),
|
||||
output_urls,
|
||||
error: parsed.error,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key and a valid request_id
|
||||
async fn test_poll_prediction_result() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = get_test_api_key()?;
|
||||
let request_id = RequestId::from_str("5fd7f575-4b9e-4c0a-b7f7-0eb036027b81"); // Corgi shiba I2V
|
||||
let request_id = RequestId::from_str("8a4b578b-4514-443f-b444-4fb3bc5c2cbb"); // Cat TTV
|
||||
let request_id = RequestId::from_str("82d622b1-8911-4cac-8362-e18efaa743ed"); // Corgi I2V
|
||||
let request_id = RequestId::from_str("671c2dee-2ca9-47d7-a12e-d16610b1b365"); // Corgi TTV #1
|
||||
let request_id = RequestId::from_str("8c5303f6-b602-433c-bae8-2d3ef915b7bb"); // Corgi TTV #2
|
||||
|
||||
let result = poll_prediction_result(PollPredictionResultArgs {
|
||||
session: &session,
|
||||
request_id: &request_id,
|
||||
}).await?;
|
||||
println!("Status: {:?}", result.status);
|
||||
println!("Output URLs: {:?}", result.output_urls);
|
||||
println!("Error: {:?}", result.error);
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct PollPredictionResultResponse {
|
||||
pub status: String,
|
||||
pub outputs: Option<Vec<String>>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod seedance_2p0_image_to_video;
|
||||
mod request_types;
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
// --- Request ---
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub(super) struct Seedance2p0I2vRequest {
|
||||
pub prompt: String,
|
||||
pub images_list: Vec<String>,
|
||||
pub aspect_ratio: &'static str,
|
||||
pub duration: u8,
|
||||
pub quality: &'static str,
|
||||
}
|
||||
|
||||
// --- Response ---
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct Seedance2p0I2vResponse {
|
||||
pub request_id: String,
|
||||
}
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
use crate::api_types::request_id::RequestId;
|
||||
use crate::creds::muapi_session::MuapiSession;
|
||||
use crate::error::muapi_client_error::MuapiClientError;
|
||||
use crate::error::muapi_error::MuapiError;
|
||||
use crate::error::muapi_generic_api_error::MuapiGenericApiError;
|
||||
use crate::requests::seedance_2p0_image_to_video::request_types::*;
|
||||
use log::info;
|
||||
use wreq::Client;
|
||||
|
||||
const SEEDANCE_2P0_I2V_URL: &str = "https://api.muapi.ai/api/v1/seedance-v2.0-i2v";
|
||||
|
||||
// --- Public enums ---
|
||||
|
||||
/// Video aspect ratio.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AspectRatio {
|
||||
/// 16:9 landscape
|
||||
Landscape16x9,
|
||||
/// 9:16 portrait
|
||||
Portrait9x16,
|
||||
/// 4:3 standard
|
||||
Standard4x3,
|
||||
/// 3:4 portrait
|
||||
Portrait3x4,
|
||||
}
|
||||
|
||||
impl AspectRatio {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Landscape16x9 => "16:9",
|
||||
Self::Portrait9x16 => "9:16",
|
||||
Self::Standard4x3 => "4:3",
|
||||
Self::Portrait3x4 => "3:4",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Video duration.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Duration {
|
||||
/// 5 seconds
|
||||
FiveSeconds,
|
||||
/// 10 seconds
|
||||
TenSeconds,
|
||||
/// 15 seconds
|
||||
FifteenSeconds,
|
||||
}
|
||||
|
||||
impl Duration {
|
||||
fn as_u8(&self) -> u8 {
|
||||
match self {
|
||||
Self::FiveSeconds => 5,
|
||||
Self::TenSeconds => 10,
|
||||
Self::FifteenSeconds => 15,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Video quality tier.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Quality {
|
||||
Basic,
|
||||
High,
|
||||
}
|
||||
|
||||
impl Quality {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Basic => "basic",
|
||||
Self::High => "high",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Args & response ---
|
||||
|
||||
pub struct Seedance2p0ImageToVideoArgs<'a> {
|
||||
pub session: &'a MuapiSession,
|
||||
|
||||
/// The prompt describing the desired video.
|
||||
pub prompt: String,
|
||||
|
||||
/// One or more image URLs to use as input frames.
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
/// The aspect ratio for the output video.
|
||||
pub aspect_ratio: AspectRatio,
|
||||
|
||||
/// Duration of the generated video.
|
||||
pub duration: Duration,
|
||||
|
||||
/// Quality tier.
|
||||
pub quality: Quality,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Seedance2p0ImageToVideoArgs<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Seedance2p0ImageToVideoArgs")
|
||||
.field("prompt", &self.prompt)
|
||||
.field("image_urls", &self.image_urls)
|
||||
.field("aspect_ratio", &self.aspect_ratio)
|
||||
.field("duration", &self.duration)
|
||||
.field("quality", &self.quality)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Seedance2p0ImageToVideoResponse {
|
||||
/// The request ID used to poll for results.
|
||||
pub request_id: RequestId,
|
||||
}
|
||||
|
||||
// --- Implementation ---
|
||||
|
||||
pub async fn seedance_2p0_image_to_video(
|
||||
args: Seedance2p0ImageToVideoArgs<'_>,
|
||||
) -> Result<Seedance2p0ImageToVideoResponse, MuapiError> {
|
||||
info!("Submitting Seedance 2.0 i2v task to Muapi: {:?}", args);
|
||||
|
||||
let request_body = Seedance2p0I2vRequest {
|
||||
prompt: args.prompt,
|
||||
images_list: args.image_urls,
|
||||
aspect_ratio: args.aspect_ratio.as_str(),
|
||||
duration: args.duration.as_u8(),
|
||||
quality: args.quality.as_str(),
|
||||
};
|
||||
|
||||
info!("Muapi request body: {:?}", request_body);
|
||||
|
||||
let api_key = args.session.api_key.as_str();
|
||||
|
||||
let client = Client::builder()
|
||||
.build()
|
||||
.map_err(|err| MuapiClientError::WreqClientError(err))?;
|
||||
|
||||
let response = client.post(SEEDANCE_2P0_I2V_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("x-api-key", api_key)
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| MuapiGenericApiError::WreqError(err))?;
|
||||
|
||||
let status = response.status();
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| MuapiGenericApiError::WreqError(err))?;
|
||||
|
||||
info!("Muapi response status: {}, body: {}", status, response_body);
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(MuapiGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: status,
|
||||
body: response_body,
|
||||
}.into());
|
||||
}
|
||||
|
||||
let parsed: Seedance2p0I2vResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| MuapiGenericApiError::SerdeResponseParseErrorWithBody(err, response_body))?;
|
||||
|
||||
Ok(Seedance2p0ImageToVideoResponse {
|
||||
request_id: RequestId::new(parsed.request_id),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
use test_data::web::image_urls::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key
|
||||
async fn test_seedance_2p0_image_to_video() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = get_test_api_key()?;
|
||||
let args = Seedance2p0ImageToVideoArgs {
|
||||
session: &session,
|
||||
prompt: "The dog barks and runs across the lake's pier, tail wagging.".to_string(),
|
||||
image_urls: vec![
|
||||
JUNO_AT_LAKE_IMAGE_URL.to_string(),
|
||||
],
|
||||
aspect_ratio: AspectRatio::Landscape16x9,
|
||||
duration: Duration::FiveSeconds,
|
||||
quality: Quality::High,
|
||||
};
|
||||
let result = seedance_2p0_image_to_video(args).await?;
|
||||
println!("Result: {:?}", result);
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.as_str().is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key
|
||||
async fn test_seedance_2p0_image_to_video_portrait() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = get_test_api_key()?;
|
||||
let args = Seedance2p0ImageToVideoArgs {
|
||||
session: &session,
|
||||
prompt: "The corgi and shiba swim through the treasure-filled ocean, bubbles rising around them.".to_string(),
|
||||
image_urls: vec![
|
||||
TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(),
|
||||
],
|
||||
aspect_ratio: AspectRatio::Portrait9x16,
|
||||
duration: Duration::FiveSeconds,
|
||||
quality: Quality::High,
|
||||
};
|
||||
let result = seedance_2p0_image_to_video(args).await?;
|
||||
println!("Result: {:?}", result);
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.as_str().is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod seedance_2p0_text_to_video;
|
||||
mod request_types;
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
// --- Request ---
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub(super) struct Seedance2p0T2vRequest {
|
||||
pub prompt: String,
|
||||
pub aspect_ratio: &'static str,
|
||||
pub duration: u8,
|
||||
pub quality: &'static str,
|
||||
}
|
||||
|
||||
// --- Response ---
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct Seedance2p0T2vResponse {
|
||||
pub request_id: String,
|
||||
}
|
||||
-209
@@ -1,209 +0,0 @@
|
||||
use crate::api_types::request_id::RequestId;
|
||||
use crate::creds::muapi_session::MuapiSession;
|
||||
use crate::error::muapi_client_error::MuapiClientError;
|
||||
use crate::error::muapi_error::MuapiError;
|
||||
use crate::error::muapi_generic_api_error::MuapiGenericApiError;
|
||||
use crate::requests::seedance_2p0_text_to_video::request_types::*;
|
||||
use log::info;
|
||||
use wreq::Client;
|
||||
|
||||
const SEEDANCE_2P0_T2V_URL: &str = "https://api.muapi.ai/api/v1/seedance-v2.0-t2v";
|
||||
|
||||
// --- Public enums ---
|
||||
|
||||
/// Video aspect ratio.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AspectRatio {
|
||||
/// 16:9 landscape
|
||||
Landscape16x9,
|
||||
/// 9:16 portrait
|
||||
Portrait9x16,
|
||||
/// 4:3 standard
|
||||
Standard4x3,
|
||||
/// 3:4 portrait
|
||||
Portrait3x4,
|
||||
}
|
||||
|
||||
impl AspectRatio {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Landscape16x9 => "16:9",
|
||||
Self::Portrait9x16 => "9:16",
|
||||
Self::Standard4x3 => "4:3",
|
||||
Self::Portrait3x4 => "3:4",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Video duration.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Duration {
|
||||
/// 5 seconds
|
||||
FiveSeconds,
|
||||
/// 10 seconds
|
||||
TenSeconds,
|
||||
/// 15 seconds
|
||||
FifteenSeconds,
|
||||
}
|
||||
|
||||
impl Duration {
|
||||
fn as_u8(&self) -> u8 {
|
||||
match self {
|
||||
Self::FiveSeconds => 5,
|
||||
Self::TenSeconds => 10,
|
||||
Self::FifteenSeconds => 15,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Video quality tier.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Quality {
|
||||
Basic,
|
||||
High,
|
||||
}
|
||||
|
||||
impl Quality {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Basic => "basic",
|
||||
Self::High => "high",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Args & response ---
|
||||
|
||||
pub struct Seedance2p0TextToVideoArgs<'a> {
|
||||
pub session: &'a MuapiSession,
|
||||
|
||||
/// The prompt describing the desired video.
|
||||
pub prompt: String,
|
||||
|
||||
/// The aspect ratio for the output video.
|
||||
pub aspect_ratio: AspectRatio,
|
||||
|
||||
/// Duration of the generated video.
|
||||
pub duration: Duration,
|
||||
|
||||
/// Quality tier.
|
||||
pub quality: Quality,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Seedance2p0TextToVideoArgs<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Seedance2p0TextToVideoArgs")
|
||||
.field("prompt", &self.prompt)
|
||||
.field("aspect_ratio", &self.aspect_ratio)
|
||||
.field("duration", &self.duration)
|
||||
.field("quality", &self.quality)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Seedance2p0TextToVideoResponse {
|
||||
/// The request ID used to poll for results.
|
||||
pub request_id: RequestId,
|
||||
}
|
||||
|
||||
// --- Implementation ---
|
||||
|
||||
pub async fn seedance_2p0_text_to_video(
|
||||
args: Seedance2p0TextToVideoArgs<'_>,
|
||||
) -> Result<Seedance2p0TextToVideoResponse, MuapiError> {
|
||||
info!("Submitting Seedance 2.0 t2v task to Muapi: {:?}", args);
|
||||
|
||||
let request_body = Seedance2p0T2vRequest {
|
||||
prompt: args.prompt,
|
||||
aspect_ratio: args.aspect_ratio.as_str(),
|
||||
duration: args.duration.as_u8(),
|
||||
quality: args.quality.as_str(),
|
||||
};
|
||||
|
||||
info!("Muapi request body: {:?}", request_body);
|
||||
|
||||
let api_key = args.session.api_key.as_str();
|
||||
|
||||
let client = Client::builder()
|
||||
.build()
|
||||
.map_err(|err| MuapiClientError::WreqClientError(err))?;
|
||||
|
||||
let response = client.post(SEEDANCE_2P0_T2V_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("x-api-key", api_key)
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| MuapiGenericApiError::WreqError(err))?;
|
||||
|
||||
let status = response.status();
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| MuapiGenericApiError::WreqError(err))?;
|
||||
|
||||
info!("Muapi response status: {}, body: {}", status, response_body);
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(MuapiGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: status,
|
||||
body: response_body,
|
||||
}.into());
|
||||
}
|
||||
|
||||
let parsed: Seedance2p0T2vResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| MuapiGenericApiError::SerdeResponseParseErrorWithBody(err, response_body))?;
|
||||
|
||||
Ok(Seedance2p0TextToVideoResponse {
|
||||
request_id: RequestId::new(parsed.request_id),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key
|
||||
async fn test_seedance_2p0_text_to_video_landscape() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = get_test_api_key()?;
|
||||
let args = Seedance2p0TextToVideoArgs {
|
||||
session: &session,
|
||||
prompt: "A corgi walks past a lighthouse on the shore. In the background, mossy rocks and rolling green hills dot the landscape. The sand is black, and the air is full of fog and mist".to_string(),
|
||||
aspect_ratio: AspectRatio::Landscape16x9,
|
||||
duration: Duration::FiveSeconds,
|
||||
quality: Quality::High,
|
||||
};
|
||||
let result = seedance_2p0_text_to_video(args).await?;
|
||||
println!("Result: {:?}", result);
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.as_str().is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key
|
||||
async fn test_seedance_2p0_text_to_video_portrait() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = get_test_api_key()?;
|
||||
let args = Seedance2p0TextToVideoArgs {
|
||||
session: &session,
|
||||
prompt: "A cat sits on a windowsill watching rain fall outside. Lightning flashes and the cat's eyes widen.".to_string(),
|
||||
aspect_ratio: AspectRatio::Portrait9x16,
|
||||
duration: Duration::TenSeconds,
|
||||
quality: Quality::Basic,
|
||||
};
|
||||
let result = seedance_2p0_text_to_video(args).await?;
|
||||
println!("Result: {:?}", result);
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.as_str().is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use crate::creds::muapi_session::MuapiSession;
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn get_test_api_key() -> AnyhowResult<MuapiSession> {
|
||||
let api_key = read_to_string("/Users/bt/Artcraft/credentials/muapi_api_key.txt")?;
|
||||
let api_key = api_key.trim().to_string();
|
||||
Ok(MuapiSession::from_api_key_string(api_key))
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#[cfg(test)]
|
||||
pub (crate) mod get_test_api_key;
|
||||
|
||||
#[cfg(test)]
|
||||
pub (crate) mod setup_test_logging;
|
||||
@@ -1,52 +0,0 @@
|
||||
use chrono::Local;
|
||||
use env_logger::Builder;
|
||||
use log::{debug, error, info, trace, warn, LevelFilter};
|
||||
use std::io::Write;
|
||||
|
||||
const RUST_LOG: &str = "RUST_LOG";
|
||||
|
||||
pub fn setup_test_logging(level: LevelFilter) {
|
||||
let level_str = get_level_str(level);
|
||||
|
||||
println!("Previous log level: {:?}", env::var(RUST_LOG));
|
||||
|
||||
let is_unsafe = env::set_var(RUST_LOG, level_str);
|
||||
|
||||
if is_unsafe.is_none() {
|
||||
println!("Unsafely setting Rust Log Level...");
|
||||
unsafe { std::env::set_var(RUST_LOG, level_str) }
|
||||
}
|
||||
|
||||
println!("Log level: {:?}", env::var(RUST_LOG));
|
||||
|
||||
Builder::new()
|
||||
.is_test(true)
|
||||
.format(|buf, record| {
|
||||
writeln!(buf,
|
||||
"{} [{}] - {}",
|
||||
Local::now().format("%H:%M:%S%.6f"),
|
||||
record.level(),
|
||||
record.args()
|
||||
)
|
||||
})
|
||||
.filter(None, level)
|
||||
.filter_level(level)
|
||||
.init();
|
||||
|
||||
trace!("Test trace log");
|
||||
debug!("Test debug log");
|
||||
info!("Test info log");
|
||||
warn!("Test warn log");
|
||||
error!("Test error log");
|
||||
}
|
||||
|
||||
fn get_level_str(level: LevelFilter) -> &'static str {
|
||||
match level {
|
||||
LevelFilter::Error => "error",
|
||||
LevelFilter::Warn => "warn",
|
||||
LevelFilter::Info => "info",
|
||||
LevelFilter::Debug => "debug",
|
||||
LevelFilter::Trace => "trace",
|
||||
LevelFilter::Off => "off",
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub const FIREFOX_USER_AGENT: &str =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:147.0) Gecko/20100101 Firefox/147.0";
|
||||
@@ -1 +0,0 @@
|
||||
pub mod common_headers;
|
||||
@@ -31,6 +31,7 @@ wreq.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
chrono.workspace = true
|
||||
env.workspace = true
|
||||
env_logger.workspace = true
|
||||
testing = { path = "../../lib/testing" }
|
||||
|
||||
@@ -14,17 +14,10 @@ pub enum Seedance2ProError {
|
||||
impl Seedance2ProError {
|
||||
pub fn is_having_downtime_issues(&self) -> bool {
|
||||
match self {
|
||||
Self::ApiGeneric(Seedance2ProGenericApiError::Http502ErrorBadGateway(_)) => true,
|
||||
Self::ApiGeneric(Seedance2ProGenericApiError::CloudflareError(CloudflareError::BadGateway502)) => true,
|
||||
Self::ApiGeneric(Seedance2ProGenericApiError::CloudflareError(CloudflareError::GatewayTimeout504)) => true,
|
||||
Self::ApiGeneric(Seedance2ProGenericApiError::CloudflareError(CloudflareError::TimeoutOccurred524)) => true,
|
||||
Self::ApiGeneric(Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body: _ }) => {
|
||||
match status_code.as_u16() {
|
||||
502 => true,
|
||||
504 => true,
|
||||
524 => true,
|
||||
_ => false,
|
||||
}
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ pub enum Seedance2ProGenericApiError {
|
||||
/// Specific Cloudflare errors.
|
||||
CloudflareError(CloudflareError),
|
||||
|
||||
/// HTTP 502 Bad Gateway.
|
||||
/// We preserve the message for debugging.
|
||||
Http502ErrorBadGateway(String),
|
||||
|
||||
/// serde_json::Error, likely from JSON deserialization schema mismatch.
|
||||
/// Includes the original body.
|
||||
SerdeResponseParseErrorWithBody(serde_json::Error, String),
|
||||
@@ -35,6 +39,7 @@ impl Display for Seedance2ProGenericApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CloudflareError(err) => write!(f, "Cloudflare error: {}", err),
|
||||
Self::Http502ErrorBadGateway(msg) => write!(f, "HTTP 502 Bad Gateway: {}", msg),
|
||||
Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body),
|
||||
Self::SerdeParseErrorWithBodyOnNon200(err, body) => write!(f, "Failed to parse non-200 response body: {:?}. Body: {}", err, body),
|
||||
Self::UncategorizedBadResponse(msg) => write!(f, "Uncategorized bad response: {}", msg),
|
||||
|
||||
@@ -31,40 +31,8 @@ pub struct GenerateVideoArgs<'a> {
|
||||
/// Optional end frame image URL (keyframe mode).
|
||||
pub end_frame_url: Option<String>,
|
||||
|
||||
/// Optional reference image URLs (reference mode).
|
||||
/// When present, takes priority over start/end frames.
|
||||
/// Optional reference image URLs (reference mode). When present, takes priority over start/end frames.
|
||||
pub reference_image_urls: Option<Vec<String>>,
|
||||
|
||||
/// Optional reference video URLs (reference mode).
|
||||
/// Can be combined with reference_image_urls.
|
||||
/// Videos are referenced in prompts as @video1, @video2, etc.
|
||||
/// When present, takes priority over start/end frames.
|
||||
pub reference_video_urls: Option<Vec<String>>,
|
||||
|
||||
/// Optional reference audio URLs (reference mode).
|
||||
/// Audio is referenced in prompts as @audio1, @audio2, etc.
|
||||
/// Sent in a separate `audioUrls` field (not in `uploadedUrls`).
|
||||
pub reference_audio_urls: Option<Vec<String>>,
|
||||
|
||||
/// Controls the `faceBlurMode` field: true sends "on", false sends "off", None omits it.
|
||||
pub use_face_blur_hack: Option<bool>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GenerateVideoArgs<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("GenerateVideoArgs")
|
||||
.field("prompt", &self.prompt)
|
||||
.field("resolution", &self.resolution)
|
||||
.field("duration_seconds", &self.duration_seconds)
|
||||
.field("batch_count", &self.batch_count)
|
||||
.field("start_frame_url", &self.start_frame_url)
|
||||
.field("end_frame_url", &self.end_frame_url)
|
||||
.field("reference_image_urls", &self.reference_image_urls)
|
||||
.field("reference_video_urls", &self.reference_video_urls)
|
||||
.field("reference_audio_urls", &self.reference_audio_urls)
|
||||
.field("use_face_blur_hack", &self.use_face_blur_hack)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateVideoArgs<'_> {
|
||||
@@ -158,26 +126,12 @@ pub struct GenerateVideoResponse {
|
||||
// --- Implementation ---
|
||||
|
||||
pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideoResponse, Seedance2ProError> {
|
||||
|
||||
info!("Requesting video from Seedance2Pro: {:?}", args);
|
||||
|
||||
let has_reference_images = args.reference_image_urls.as_ref().is_some_and(|urls| !urls.is_empty());
|
||||
let has_reference_videos = args.reference_video_urls.as_ref().is_some_and(|urls| !urls.is_empty());
|
||||
let has_reference_audio = args.reference_audio_urls.as_ref().is_some_and(|urls| !urls.is_empty());
|
||||
|
||||
let is_reference_mode = has_reference_images || has_reference_videos || has_reference_audio;
|
||||
let is_reference_mode = args.reference_image_urls.as_ref().is_some_and(|urls| !urls.is_empty());
|
||||
|
||||
let video_input_mode = if is_reference_mode { "reference" } else { "keyframe" };
|
||||
|
||||
let uploaded_urls: Option<Vec<String>> = if is_reference_mode {
|
||||
let mut urls = Vec::new();
|
||||
if let Some(video_urls) = args.reference_video_urls {
|
||||
urls.extend(video_urls);
|
||||
}
|
||||
if let Some(image_urls) = args.reference_image_urls {
|
||||
urls.extend(image_urls);
|
||||
}
|
||||
if urls.is_empty() { None } else { Some(urls) }
|
||||
args.reference_image_urls
|
||||
} else {
|
||||
let mut urls = Vec::new();
|
||||
if let Some(url) = args.start_frame_url {
|
||||
@@ -189,18 +143,6 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
|
||||
if urls.is_empty() { None } else { Some(urls) }
|
||||
};
|
||||
|
||||
let audio_urls: Option<Vec<String>> = if has_reference_audio {
|
||||
args.reference_audio_urls
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let face_blur_mode = match args.use_face_blur_hack {
|
||||
Some(true) => Some("on"),
|
||||
Some(false) => Some("off"),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let batch_count_value = args.batch_count.as_u8();
|
||||
let batch_count = if batch_count_value > 1 { Some(batch_count_value) } else { None };
|
||||
|
||||
@@ -222,17 +164,13 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
|
||||
model: "seedance-20",
|
||||
duration,
|
||||
mode: video_input_mode,
|
||||
face_blur_mode,
|
||||
uploaded_urls,
|
||||
audio_urls,
|
||||
batch_count,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
info!("Seedance2pro request : {:?}", request_body);
|
||||
|
||||
let cookie = args.session.cookies.as_str();
|
||||
|
||||
let client = Client::builder()
|
||||
@@ -302,15 +240,12 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
use super::*;
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::test_utils::get_test_cookies::get_test_cookies;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
use crate::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
|
||||
use crate::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
|
||||
|
||||
fn dummy_session() -> Seedance2ProSession {
|
||||
Seedance2ProSession::from_cookies_string(String::new())
|
||||
@@ -328,9 +263,6 @@ mod tests {
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
use_face_blur_hack: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,9 +325,6 @@ mod tests {
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
use_face_blur_hack: None,
|
||||
};
|
||||
let result = generate_video(args).await?;
|
||||
println!("Task ID: {}", result.task_id);
|
||||
@@ -420,9 +349,6 @@ mod tests {
|
||||
start_frame_url: Some("https://static.seedance2-pro.com/materials/20260219/1771496300184-fb32e08c.jpg".to_string()),
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
use_face_blur_hack: None,
|
||||
};
|
||||
let result = generate_video(args).await?;
|
||||
println!("Task ID: {}", result.task_id);
|
||||
@@ -434,7 +360,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies
|
||||
async fn test_generate_reference_image_video() -> AnyhowResult<()> {
|
||||
async fn test_generate_reference_video() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
let args = GenerateVideoArgs {
|
||||
@@ -449,9 +375,6 @@ mod tests {
|
||||
"https://static.seedance2-pro.com/materials/20260219/1771463564512-b14bfe90.png".to_string(),
|
||||
"https://static.seedance2-pro.com/materials/20260219/1771496300184-fb32e08c.jpg".to_string(),
|
||||
]),
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
use_face_blur_hack: None,
|
||||
};
|
||||
let result = generate_video(args).await?;
|
||||
println!("Task ID: {}", result.task_id);
|
||||
@@ -460,113 +383,4 @@ mod tests {
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies
|
||||
async fn test_generate_reference_video_only() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
let args = GenerateVideoArgs {
|
||||
session: &session,
|
||||
prompt: "Change the Video @video1 to night time.".to_string(),
|
||||
resolution: Resolution::Landscape16x9,
|
||||
duration_seconds: 5,
|
||||
batch_count: BatchCount::One,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: Some(vec![
|
||||
"https://static.seedance2-pro.com/materials/20260315/1773594284659-3a46d231.mp4".to_string(),
|
||||
]),
|
||||
reference_audio_urls: None,
|
||||
use_face_blur_hack: None,
|
||||
};
|
||||
let result = generate_video(args).await?;
|
||||
println!("Task ID: {}", result.task_id);
|
||||
println!("Order ID: {}", result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies
|
||||
async fn test_generate_reference_video_and_image() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
let args = GenerateVideoArgs {
|
||||
session: &session,
|
||||
prompt: "Put the robot in @video1 next to the house in @image1".to_string(),
|
||||
resolution: Resolution::Landscape16x9,
|
||||
duration_seconds: 5,
|
||||
batch_count: BatchCount::One,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: Some(vec![
|
||||
"https://static.seedance2-pro.com/materials/20260315/1773595053724-07a1d500.png".to_string(),
|
||||
]),
|
||||
reference_video_urls: Some(vec![
|
||||
"https://static.seedance2-pro.com/materials/20260315/1773594284659-3a46d231.mp4".to_string(),
|
||||
]),
|
||||
reference_audio_urls: None,
|
||||
use_face_blur_hack: None,
|
||||
};
|
||||
let result = generate_video(args).await?;
|
||||
println!("Task ID: {}", result.task_id);
|
||||
println!("Order ID: {}", result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies and a test image
|
||||
async fn test_video_ref_file_that_is_too_long() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
|
||||
// Step 1: Get a signed upload URL
|
||||
let cookies = get_test_cookies()?;
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies);
|
||||
let prepare_args = PrepareFileUploadArgs {
|
||||
session: &session,
|
||||
extension: "mp4".to_string(),
|
||||
};
|
||||
let prepare_result = prepare_file_upload(prepare_args).await?;
|
||||
println!("Upload URL: {}", prepare_result.upload_url);
|
||||
|
||||
// Step 2: Read a test image
|
||||
let file_bytes = fs::read("/Users/bt/Videos/Artcraft/Artcraft Best/ArtCraft Seedance Knight.mp4")?;
|
||||
println!("File size: {} bytes", file_bytes.len());
|
||||
|
||||
// Step 3: Upload
|
||||
let upload_args = UploadFileArgs {
|
||||
upload_url: prepare_result.upload_url,
|
||||
file_bytes,
|
||||
};
|
||||
let result = upload_file(upload_args).await?;
|
||||
println!("Public URL: {}", result.public_url);
|
||||
|
||||
let args = GenerateVideoArgs {
|
||||
session: &session,
|
||||
prompt: "Change @video1 to night time".to_string(),
|
||||
resolution: Resolution::Landscape16x9,
|
||||
duration_seconds: 5,
|
||||
batch_count: BatchCount::One,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: Some(vec![
|
||||
result.public_url,
|
||||
]),
|
||||
reference_audio_urls: None,
|
||||
use_face_blur_hack: None,
|
||||
};
|
||||
let result = generate_video(args).await?;
|
||||
println!("Task ID: {}", result.task_id);
|
||||
println!("Order ID: {}", result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Serialize)]
|
||||
pub (super) struct BatchRequest {
|
||||
#[serde(rename = "0")]
|
||||
pub zero: BatchRequestInner,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Serialize)]
|
||||
pub (super) struct BatchRequestInner {
|
||||
pub json: BatchRequestJson,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Serialize)]
|
||||
pub (super) struct BatchRequestJson {
|
||||
#[serde(rename = "businessType")]
|
||||
pub business_type: &'static str,
|
||||
@@ -19,7 +19,7 @@ pub (super) struct BatchRequestJson {
|
||||
pub api_params: ApiParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Serialize)]
|
||||
pub (super) struct ApiParams {
|
||||
pub prompt: String,
|
||||
pub resolution: String,
|
||||
@@ -28,12 +28,8 @@ pub (super) struct ApiParams {
|
||||
pub model: &'static str,
|
||||
pub duration: String,
|
||||
pub mode: &'static str,
|
||||
#[serde(rename = "faceBlurMode", skip_serializing_if = "Option::is_none")]
|
||||
pub face_blur_mode: Option<&'static str>,
|
||||
#[serde(rename = "uploadedUrls", skip_serializing_if = "Option::is_none")]
|
||||
pub uploaded_urls: Option<Vec<String>>,
|
||||
#[serde(rename = "audioUrls", skip_serializing_if = "Option::is_none")]
|
||||
pub audio_urls: Option<Vec<String>>,
|
||||
#[serde(rename = "batchCount", skip_serializing_if = "Option::is_none")]
|
||||
pub batch_count: Option<u8>,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub mod generate_video;
|
||||
pub mod poll_orders;
|
||||
pub mod prepare_file_upload;
|
||||
pub mod upload_file;
|
||||
pub mod prepare_image_upload;
|
||||
pub mod upload_image;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
use super::failure_type::FailureType;
|
||||
|
||||
/// A structured failure reason parsed from the raw Seedance2 Pro API response.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FailureReason {
|
||||
/// The original reason string from the API.
|
||||
pub reason: String,
|
||||
|
||||
/// The categorized failure type.
|
||||
pub failure_type: FailureType,
|
||||
}
|
||||
|
||||
impl FailureReason {
|
||||
/// Parse a raw failure reason string into a structured `FailureReason`.
|
||||
///
|
||||
/// First checks for exact string matches against known reasons,
|
||||
/// then falls back to case-insensitive substring matching.
|
||||
pub fn from_reason(reason: &str) -> Self {
|
||||
let failure_type = FailureType::classify_text(reason);
|
||||
FailureReason {
|
||||
reason: reason.to_string(),
|
||||
failure_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
|
||||
/// Categorized failure type for a Seedance2 Pro order.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum FailureType {
|
||||
/// User's uploaded image was rejected by platform content rules.
|
||||
RuleBansUserImage,
|
||||
/// User's uploaded image was rejected because it contains faces.
|
||||
RuleBansUserImageWithFaces,
|
||||
/// User's text prompt was rejected by platform content rules.
|
||||
RuleBansUserTextPrompt,
|
||||
/// User's content (image or text) was rejected by platform content rules (generic).
|
||||
RuleBansUserContent,
|
||||
/// The generated video was rejected by platform content review.
|
||||
RuleBansGeneratedVideo,
|
||||
/// The generated audio was rejected by platform content rules.
|
||||
RuleBansGeneratedAudio,
|
||||
/// The generated content (video/audio/other) was rejected by platform content rules (generic).
|
||||
RuleBansGeneratedContent,
|
||||
/// Video generation failed (timeout, server error, processing error, etc.)
|
||||
GenerationFailed,
|
||||
/// An unrecognized or absent failure reason.
|
||||
OtherUnknownReason,
|
||||
}
|
||||
|
||||
impl FailureType {
|
||||
pub fn classify_text(reason: &str) -> Self {
|
||||
// --- Exact matches first ---
|
||||
match reason {
|
||||
"Your uploaded image violates platform rules. Please modify and try again." =>
|
||||
return Self::RuleBansUserImage,
|
||||
"Face detected in uploaded media. Please adjust your media and try again." =>
|
||||
return Self::RuleBansUserImageWithFaces,
|
||||
"Your input text violates platform rules. Please modify and try again." =>
|
||||
return Self::RuleBansUserTextPrompt,
|
||||
"Content violates platform rules. Please modify and try again." =>
|
||||
return Self::RuleBansUserContent,
|
||||
"The generated video did not pass review. Credits will not be deducted." =>
|
||||
return Self::RuleBansGeneratedVideo,
|
||||
"The generated audio violates platform rules. Please adjust your prompt or images and try again." =>
|
||||
return Self::RuleBansGeneratedAudio,
|
||||
"The generated content violates platform rules. Please adjust your prompt or images and try again." =>
|
||||
return Self::RuleBansGeneratedContent,
|
||||
"Video generation failed. Please try again." =>
|
||||
return Self::GenerationFailed,
|
||||
"Generation timed out. Please try again." =>
|
||||
return Self::GenerationFailed,
|
||||
"Server error. Please try again later." =>
|
||||
return Self::GenerationFailed,
|
||||
"Your content could not be processed. Please try different images or adjust your prompt." =>
|
||||
return Self::GenerationFailed,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// --- Substring matches (case-insensitive) ---
|
||||
let lower = reason.to_lowercase();
|
||||
|
||||
if lower.contains("face detected") || lower.contains("ensure no faces") {
|
||||
return Self::RuleBansUserImageWithFaces;
|
||||
}
|
||||
if lower.contains("uploaded image violates") {
|
||||
return Self::RuleBansUserImage;
|
||||
}
|
||||
if lower.contains("input text violates") {
|
||||
return Self::RuleBansUserTextPrompt;
|
||||
}
|
||||
if lower.contains("generated video") && lower.contains("not pass review") {
|
||||
return Self::RuleBansGeneratedVideo;
|
||||
}
|
||||
if lower.contains("generated audio") && lower.contains("violates") {
|
||||
return Self::RuleBansGeneratedAudio;
|
||||
}
|
||||
if lower.contains("generated content") && lower.contains("violates") {
|
||||
return Self::RuleBansGeneratedContent;
|
||||
}
|
||||
if lower.contains("content violates") || lower.contains("platform rules") {
|
||||
return Self::RuleBansUserContent;
|
||||
}
|
||||
if lower.contains("video generation failed") || lower.contains("timed out") || lower.contains("server error") {
|
||||
return Self::GenerationFailed;
|
||||
}
|
||||
|
||||
Self::OtherUnknownReason
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,2 @@
|
||||
pub mod failure_reason;
|
||||
pub mod failure_type;
|
||||
pub mod poll_orders;
|
||||
mod request_types;
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::requests::poll_orders::failure_reason::FailureReason;
|
||||
use crate::requests::poll_orders::request_types::*;
|
||||
use crate::utils::common_headers::FIREFOX_USER_AGENT;
|
||||
use log::info;
|
||||
@@ -11,6 +10,17 @@ use wreq_util::Emulation;
|
||||
|
||||
const GET_ORDERS_BASE_URL: &str = "https://seedance2-pro.com/api/trpc/userOrder.getOrders";
|
||||
|
||||
/// Builds the tRPC `input` JSON for the getOrders endpoint.
|
||||
/// When `cursor` is `Some`, it is included in the JSON payload.
|
||||
fn build_input_json(cursor: Option<u64>) -> String {
|
||||
match cursor {
|
||||
None => r#"{"0":{"json":{"limit":30,"format":null,"direction":"forward"},"meta":{"values":{"format":["undefined"]},"v":1}}}"#.to_string(),
|
||||
Some(c) => format!(
|
||||
r#"{{"0":{{"json":{{"limit":30,"format":null,"cursor":{cursor},"direction":"forward"}},"meta":{{"values":{{"format":["undefined"]}},"v":1}}}}}}"#,
|
||||
cursor = c
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Args & response ---
|
||||
|
||||
@@ -87,8 +97,8 @@ pub struct OrderStatus {
|
||||
/// Detailed result entries. Typically one entry per video.
|
||||
pub results: Vec<VideoResult>,
|
||||
|
||||
/// Structured failure reason. Populated when `task_status` is `Failed` or `fail_reason` is present.
|
||||
pub fail_reason: Option<FailureReason>,
|
||||
/// Failure reason. Populated when `task_status` is `Failed`.
|
||||
pub fail_reason: Option<String>,
|
||||
|
||||
/// ISO 8601 creation timestamp (e.g. `"2026-02-19T01:20:50.398Z"`).
|
||||
pub created_at: String,
|
||||
@@ -162,25 +172,18 @@ pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result<PollOrdersResponse,
|
||||
|
||||
let orders: Vec<OrderStatus> = json.orders
|
||||
.into_iter()
|
||||
.map(|o| {
|
||||
let task_status = TaskStatus::from_str(&o.task_status);
|
||||
let fail_reason = match (&o.fail_reason, &task_status) {
|
||||
(Some(reason), _) => Some(FailureReason::from_reason(reason)),
|
||||
(None, TaskStatus::Failed) => Some(FailureReason::from_reason("(no reason)")),
|
||||
_ => None,
|
||||
};
|
||||
OrderStatus {
|
||||
order_id: o.order_id,
|
||||
task_status,
|
||||
result_url: o.result_url,
|
||||
results: o.results.into_iter().map(|r| VideoResult {
|
||||
url: r.url,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
}).collect(),
|
||||
fail_reason,
|
||||
created_at: o.created_at,
|
||||
}
|
||||
.map(|o| OrderStatus {
|
||||
order_id: o.order_id,
|
||||
task_status: TaskStatus::from_str(&o.task_status),
|
||||
result_url: o.result_url,
|
||||
results: o.results.into_iter().map(|r| VideoResult {
|
||||
url: r.url,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
// ratio: r.ratio,
|
||||
}).collect(),
|
||||
fail_reason: o.fail_reason,
|
||||
created_at: o.created_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -189,18 +192,6 @@ pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result<PollOrdersResponse,
|
||||
Ok(PollOrdersResponse { orders, next_cursor })
|
||||
}
|
||||
|
||||
/// Builds the tRPC `input` JSON for the getOrders endpoint.
|
||||
/// When `cursor` is `Some`, it is included in the JSON payload.
|
||||
fn build_input_json(cursor: Option<u64>) -> String {
|
||||
match cursor {
|
||||
None => r#"{"0":{"json":{"limit":30,"format":null,"direction":"forward"},"meta":{"values":{"format":["undefined"]},"v":1}}}"#.to_string(),
|
||||
Some(c) => format!(
|
||||
r#"{{"0":{{"json":{{"limit":30,"format":null,"cursor":{cursor},"direction":"forward"}},"meta":{{"values":{{"format":["undefined"]}},"v":1}}}}}}"#,
|
||||
cursor = c
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod prepare_file_upload;
|
||||
mod request_types;
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod prepare_image_upload;
|
||||
mod request_types;
|
||||
+14
-39
@@ -2,7 +2,7 @@ use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::requests::prepare_file_upload::request_types::*;
|
||||
use crate::requests::prepare_image_upload::request_types::*;
|
||||
use crate::utils::common_headers::FIREFOX_USER_AGENT;
|
||||
use chrono::Utc;
|
||||
use log::info;
|
||||
@@ -13,35 +13,32 @@ use wreq_util::Emulation;
|
||||
const SIGNED_UPLOAD_URL: &str = "https://seedance2-pro.com/api/trpc/uploads.signedUploadUrl?batch=1";
|
||||
|
||||
/// Generates a material path based on the current time.
|
||||
/// Format: `materials/YYYYMMDD/<unix_millis>-<random_hex>.<extension>`
|
||||
fn generate_material_path(extension: &str) -> String {
|
||||
/// Format: `materials/YYYYMMDD/<unix_millis>-<random_hex>.png`
|
||||
fn generate_material_path() -> String {
|
||||
let now = Utc::now();
|
||||
let date_part = now.format("%Y%m%d").to_string();
|
||||
let timestamp_millis = now.timestamp_millis();
|
||||
let random_hex: u32 = rand::rng().random();
|
||||
let hex_part = format!("{:08x}", random_hex);
|
||||
format!("materials/{}/{}-{}.{}", date_part, timestamp_millis, hex_part, extension)
|
||||
format!("materials/{}/{}-{}.png", date_part, timestamp_millis, hex_part)
|
||||
}
|
||||
|
||||
pub struct PrepareFileUploadArgs<'a> {
|
||||
pub struct PrepareImageUploadArgs<'a> {
|
||||
pub session: &'a Seedance2ProSession,
|
||||
|
||||
/// File extension without the dot, e.g. "png", "mp4", "mp3".
|
||||
pub extension: String,
|
||||
}
|
||||
|
||||
pub struct PrepareFileUploadResponse {
|
||||
/// The signed URL to upload the file to (Cloudflare R2 / S3-compatible).
|
||||
pub struct PrepareImageUploadResponse {
|
||||
/// The signed URL to upload the image to (Cloudflare R2 / S3-compatible).
|
||||
pub upload_url: String,
|
||||
|
||||
/// The material path that was generated for this upload.
|
||||
pub material_path: String,
|
||||
}
|
||||
|
||||
pub async fn prepare_file_upload(args: PrepareFileUploadArgs<'_>) -> Result<PrepareFileUploadResponse, Seedance2ProError> {
|
||||
let material_path = generate_material_path(&args.extension);
|
||||
pub async fn prepare_image_upload(args: PrepareImageUploadArgs<'_>) -> Result<PrepareImageUploadResponse, Seedance2ProError> {
|
||||
let material_path = generate_material_path();
|
||||
|
||||
info!("Preparing file upload with path: {}", material_path);
|
||||
info!("Preparing image upload with path: {}", material_path);
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox143)
|
||||
@@ -106,7 +103,7 @@ pub async fn prepare_file_upload(args: PrepareFileUploadArgs<'_>) -> Result<Prep
|
||||
.data
|
||||
.json;
|
||||
|
||||
Ok(PrepareFileUploadResponse {
|
||||
Ok(PrepareImageUploadResponse {
|
||||
upload_url,
|
||||
material_path,
|
||||
})
|
||||
@@ -123,41 +120,19 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies
|
||||
async fn test_prepare_file_upload_png() -> AnyhowResult<()> {
|
||||
async fn test_prepare_image_upload() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let cookies = get_test_cookies()?;
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies);
|
||||
let args = PrepareFileUploadArgs {
|
||||
let args = PrepareImageUploadArgs {
|
||||
session: &session,
|
||||
extension: "png".to_string(),
|
||||
};
|
||||
let result = prepare_file_upload(args).await?;
|
||||
let result = prepare_image_upload(args).await?;
|
||||
println!("Upload URL: {}", result.upload_url);
|
||||
println!("Material path: {}", result.material_path);
|
||||
assert!(!result.upload_url.is_empty());
|
||||
assert!(result.upload_url.contains("cloudflarestorage.com"));
|
||||
assert!(result.material_path.starts_with("materials/"));
|
||||
assert!(result.material_path.ends_with(".png"));
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to check the response.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies
|
||||
async fn test_prepare_file_upload_mp4() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let cookies = get_test_cookies()?;
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies);
|
||||
let args = PrepareFileUploadArgs {
|
||||
session: &session,
|
||||
extension: "mp4".to_string(),
|
||||
};
|
||||
let result = prepare_file_upload(args).await?;
|
||||
println!("Upload URL: {}", result.upload_url);
|
||||
println!("Material path: {}", result.material_path);
|
||||
assert!(!result.upload_url.is_empty());
|
||||
assert!(result.material_path.ends_with(".mp4"));
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to check the response.
|
||||
Ok(())
|
||||
@@ -1 +0,0 @@
|
||||
pub mod upload_file;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod upload_image;
|
||||
+20
-54
@@ -9,16 +9,16 @@ use wreq_util::Emulation;
|
||||
|
||||
const STATIC_BASE_URL: &str = "https://static.seedance2-pro.com";
|
||||
|
||||
pub struct UploadFileArgs {
|
||||
/// The signed upload URL returned by `prepare_file_upload`.
|
||||
pub struct UploadImageArgs {
|
||||
/// The signed upload URL returned by `prepare_image_upload`.
|
||||
pub upload_url: String,
|
||||
|
||||
/// The raw file bytes to upload.
|
||||
pub file_bytes: Vec<u8>,
|
||||
/// The raw image bytes to upload.
|
||||
pub image_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct UploadFileResponse {
|
||||
/// The public-facing URL for the uploaded file.
|
||||
pub struct UploadImageResponse {
|
||||
/// The public-facing URL for the uploaded image.
|
||||
/// e.g. `https://static.seedance2-pro.com/materials/20260219/1771463564512-b14bfe90.png`
|
||||
pub public_url: String,
|
||||
}
|
||||
@@ -33,8 +33,8 @@ fn build_public_url(upload_url: &str) -> Result<String, Seedance2ProError> {
|
||||
Ok(format!("{}{}", STATIC_BASE_URL, path))
|
||||
}
|
||||
|
||||
pub async fn upload_file(args: UploadFileArgs) -> Result<UploadFileResponse, Seedance2ProError> {
|
||||
info!("Uploading file to: {}", args.upload_url);
|
||||
pub async fn upload_image(args: UploadImageArgs) -> Result<UploadImageResponse, Seedance2ProError> {
|
||||
info!("Uploading image to: {}", args.upload_url);
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox143)
|
||||
@@ -53,7 +53,7 @@ pub async fn upload_file(args: UploadFileArgs) -> Result<UploadFileResponse, See
|
||||
.header("Sec-Fetch-Mode", "cors")
|
||||
.header("Sec-Fetch-Site", "cross-site")
|
||||
.header("Priority", "u=4")
|
||||
.body(args.file_bytes)
|
||||
.body(args.image_bytes)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| Seedance2ProGenericApiError::WreqError(err))?;
|
||||
@@ -77,7 +77,7 @@ pub async fn upload_file(args: UploadFileArgs) -> Result<UploadFileResponse, See
|
||||
|
||||
info!("Public URL: {}", public_url);
|
||||
|
||||
Ok(UploadFileResponse { public_url })
|
||||
Ok(UploadImageResponse { public_url })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -86,8 +86,8 @@ mod tests {
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::test_utils::get_test_cookies::get_test_cookies;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use crate::requests::prepare_file_upload::prepare_file_upload::{
|
||||
prepare_file_upload, PrepareFileUploadArgs,
|
||||
use crate::requests::prepare_image_upload::prepare_image_upload::{
|
||||
prepare_image_upload, PrepareImageUploadArgs,
|
||||
};
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
@@ -95,62 +95,28 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies and a test image
|
||||
async fn test_upload_image_file() -> AnyhowResult<()> {
|
||||
async fn test_upload_image() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
|
||||
// Step 1: Get a signed upload URL
|
||||
let cookies = get_test_cookies()?;
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies);
|
||||
let prepare_args = PrepareFileUploadArgs {
|
||||
let prepare_args = PrepareImageUploadArgs {
|
||||
session: &session,
|
||||
extension: "jpg".to_string(),
|
||||
};
|
||||
let prepare_result = prepare_file_upload(prepare_args).await?;
|
||||
let prepare_result = prepare_image_upload(prepare_args).await?;
|
||||
println!("Upload URL: {}", prepare_result.upload_url);
|
||||
|
||||
// Step 2: Read a test image
|
||||
let file_bytes = fs::read("/Users/bt/dev/storyteller/artcraft/test_data/image/juno.jpg")?;
|
||||
println!("File size: {} bytes", file_bytes.len());
|
||||
let image_bytes = fs::read("/Users/bt/dev/storyteller/artcraft/test_data/image/juno.jpg")?;
|
||||
println!("Image size: {} bytes", image_bytes.len());
|
||||
|
||||
// Step 3: Upload
|
||||
let upload_args = UploadFileArgs {
|
||||
let upload_args = UploadImageArgs {
|
||||
upload_url: prepare_result.upload_url,
|
||||
file_bytes,
|
||||
image_bytes,
|
||||
};
|
||||
let result = upload_file(upload_args).await?;
|
||||
println!("Public URL: {}", result.public_url);
|
||||
|
||||
assert!(result.public_url.starts_with("https://static.seedance2-pro.com/materials/"));
|
||||
assert_eq!(1, 2); // NB: Intentional failure to check the response.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies and a test image
|
||||
async fn test_upload_video_file_that_is_too_long() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
|
||||
// Step 1: Get a signed upload URL
|
||||
let cookies = get_test_cookies()?;
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies);
|
||||
let prepare_args = PrepareFileUploadArgs {
|
||||
session: &session,
|
||||
extension: "mp4".to_string(),
|
||||
};
|
||||
let prepare_result = prepare_file_upload(prepare_args).await?;
|
||||
println!("Upload URL: {}", prepare_result.upload_url);
|
||||
|
||||
// Step 2: Read a test image
|
||||
let file_bytes = fs::read("/Users/bt/Videos/Artcraft/Artcraft Best/ArtCraft Seedance Knight.mp4")?;
|
||||
println!("File size: {} bytes", file_bytes.len());
|
||||
|
||||
// Step 3: Upload
|
||||
let upload_args = UploadFileArgs {
|
||||
upload_url: prepare_result.upload_url,
|
||||
file_bytes,
|
||||
};
|
||||
let result = upload_file(upload_args).await?;
|
||||
let result = upload_image(upload_args).await?;
|
||||
println!("Public URL: {}", result.public_url);
|
||||
|
||||
assert!(result.public_url.starts_with("https://static.seedance2-pro.com/materials/"));
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "support-tool"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
|
||||
[[bin]]
|
||||
name = "support-tool"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
easyenv.workspace = true
|
||||
|
||||
# Internal
|
||||
errors.workspace = true
|
||||
seedance2pro.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
log.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod run;
|
||||
pub mod seedance2pro;
|
||||
@@ -1,36 +0,0 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use super::seedance2pro;
|
||||
|
||||
/// All canonical subcommand names across all modules.
|
||||
/// Used by the underscore-insensitive arg normalizer.
|
||||
pub fn all_canonical_names() -> Vec<&'static str> {
|
||||
let mut names: Vec<&str> = vec!["seedance2pro"];
|
||||
names.extend_from_slice(seedance2pro::dispatch::SUBCOMMAND_NAMES);
|
||||
names
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "support-tool", about = "Production support CLI")]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: TopLevelCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
#[command(rename_all = "snake_case")]
|
||||
pub enum TopLevelCommand {
|
||||
/// Seedance2 Pro support commands
|
||||
Seedance2pro {
|
||||
#[command(subcommand)]
|
||||
command: seedance2pro::Seedance2proCommand,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run(cli: Cli) -> anyhow::Result<()> {
|
||||
match cli.command {
|
||||
TopLevelCommand::Seedance2pro { command } => {
|
||||
seedance2pro::run(command).await
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use clap::Subcommand;
|
||||
|
||||
use super::state::Seedance2ProState;
|
||||
use super::subcommands;
|
||||
|
||||
/// All canonical subcommand names for this module.
|
||||
/// Used by the underscore-insensitive arg normalizer.
|
||||
pub const SUBCOMMAND_NAMES: &[&str] = &["find_job", "failed_job_histogram"];
|
||||
|
||||
#[derive(Subcommand)]
|
||||
#[command(rename_all = "snake_case")]
|
||||
pub enum Seedance2proCommand {
|
||||
/// Find a job by its order ID across all pages
|
||||
FindJob(subcommands::find_job::FindJobArgs),
|
||||
|
||||
/// Scan all jobs and print a histogram of failure reasons
|
||||
FailedJobHistogram,
|
||||
}
|
||||
|
||||
pub async fn run(command: Seedance2proCommand) -> anyhow::Result<()> {
|
||||
let cookies = easyenv::get_env_string_required("SEEDANCE2PRO_COOKIES")
|
||||
.map_err(|err| anyhow!("Missing SEEDANCE2PRO_COOKIES env var: {:?}", err))?;
|
||||
|
||||
let state = Seedance2ProState { cookies };
|
||||
|
||||
match command {
|
||||
Seedance2proCommand::FindJob(args) => subcommands::find_job::run(&state, args).await,
|
||||
Seedance2proCommand::FailedJobHistogram => subcommands::failed_job_histogram::run(&state).await,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
pub mod dispatch;
|
||||
mod state;
|
||||
mod subcommands;
|
||||
|
||||
pub use dispatch::Seedance2proCommand;
|
||||
pub use dispatch::run;
|
||||
@@ -1,3 +0,0 @@
|
||||
pub struct Seedance2ProState {
|
||||
pub cookies: String,
|
||||
}
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use log::info;
|
||||
use seedance2pro::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro::requests::poll_orders::failure_type::FailureType;
|
||||
use seedance2pro::requests::poll_orders::poll_orders::{poll_orders, PollOrdersArgs, TaskStatus};
|
||||
|
||||
use super::super::state::Seedance2ProState;
|
||||
|
||||
pub async fn run(state: &Seedance2ProState) -> anyhow::Result<()> {
|
||||
let session = Seedance2ProSession::from_cookies_string(state.cookies.clone());
|
||||
|
||||
let mut cursor: Option<u64> = None;
|
||||
let mut page = 0usize;
|
||||
let mut total_orders = 0usize;
|
||||
let mut failed_count = 0usize;
|
||||
let mut histogram: BTreeMap<String, usize> = BTreeMap::new();
|
||||
|
||||
loop {
|
||||
page += 1;
|
||||
info!("Fetching page {} (cursor: {:?})...", page, cursor);
|
||||
|
||||
let result = poll_orders(PollOrdersArgs {
|
||||
session: &session,
|
||||
cursor,
|
||||
}).await
|
||||
.map_err(|err| anyhow!("Error polling orders on page {}: {:?}", page, err))?;
|
||||
|
||||
let page_count = result.orders.len();
|
||||
total_orders += page_count;
|
||||
|
||||
for order in &result.orders {
|
||||
if order.task_status == TaskStatus::Failed {
|
||||
failed_count += 1;
|
||||
let reason = order.fail_reason.as_ref()
|
||||
.map(|fr| fr.reason.as_str())
|
||||
.unwrap_or("(no reason)");
|
||||
*histogram.entry(reason.to_string()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
info!("Page {}: {} orders ({} failed so far).", page, page_count, failed_count);
|
||||
|
||||
cursor = result.next_cursor;
|
||||
if cursor.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nScanned {} orders across {} pages.", total_orders, page);
|
||||
println!("Failed: {}\n", failed_count);
|
||||
|
||||
if histogram.is_empty() {
|
||||
println!("No failed jobs found.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Sort by count descending.
|
||||
let mut sorted: Vec<(&String, &usize)> = histogram.iter().collect();
|
||||
sorted.sort_by(|a, b| b.1.cmp(a.1));
|
||||
|
||||
println!("{:<6} {}", "Count", "Failure Reason");
|
||||
println!("{:<6} {}", "-----", "--------------");
|
||||
for (reason, count) in &sorted {
|
||||
println!("{:<6} {}", count, reason);
|
||||
}
|
||||
|
||||
// Build a second histogram by FailureType.
|
||||
let mut type_histogram: BTreeMap<String, usize> = BTreeMap::new();
|
||||
for (reason, count) in &histogram {
|
||||
let failure_type = FailureType::classify_text(reason);
|
||||
*type_histogram.entry(format!("{:?}", failure_type)).or_insert(0) += count;
|
||||
}
|
||||
|
||||
let mut type_sorted: Vec<(&String, &usize)> = type_histogram.iter().collect();
|
||||
type_sorted.sort_by(|a, b| b.1.cmp(a.1));
|
||||
|
||||
println!("\n{:<6} {}", "Count", "Failure Type");
|
||||
println!("{:<6} {}", "-----", "------------");
|
||||
for (failure_type, count) in &type_sorted {
|
||||
println!("{:<6} {}", count, failure_type);
|
||||
}
|
||||
|
||||
// Print reason → type mapping table.
|
||||
println!("\n{:<30} {}", "Failure Type", "Failure Reason");
|
||||
println!("{:<30} {}", "------------", "--------------");
|
||||
for (reason, _) in &sorted {
|
||||
let failure_type = FailureType::classify_text(reason);
|
||||
println!("{:<30} {}", format!("{:?}", failure_type), reason);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use clap::Args;
|
||||
use log::info;
|
||||
use seedance2pro::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro::requests::poll_orders::poll_orders::{poll_orders, PollOrdersArgs};
|
||||
|
||||
use super::super::state::Seedance2ProState;
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct FindJobArgs {
|
||||
/// The order ID (job token) to search for
|
||||
#[arg(long)]
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
pub async fn run(state: &Seedance2ProState, args: FindJobArgs) -> anyhow::Result<()> {
|
||||
let session = Seedance2ProSession::from_cookies_string(state.cookies.clone());
|
||||
|
||||
let mut cursor: Option<u64> = None;
|
||||
let mut page = 0usize;
|
||||
|
||||
loop {
|
||||
page += 1;
|
||||
info!("Fetching page {} (cursor: {:?})...", page, cursor);
|
||||
|
||||
let result = poll_orders(PollOrdersArgs {
|
||||
session: &session,
|
||||
cursor,
|
||||
}).await
|
||||
.map_err(|err| anyhow!("Error polling orders on page {}: {:?}", page, err))?;
|
||||
|
||||
for order in &result.orders {
|
||||
if order.order_id == args.token {
|
||||
println!("{}", serde_json::to_string_pretty(&order_to_json(order))?);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
info!("Page {}: checked {} orders, no match.", page, result.orders.len());
|
||||
|
||||
cursor = result.next_cursor;
|
||||
if cursor.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("Order '{}' not found after {} pages.", args.token, page);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
fn order_to_json(order: &seedance2pro::requests::poll_orders::poll_orders::OrderStatus) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"order_id": order.order_id,
|
||||
"task_status": format!("{:?}", order.task_status),
|
||||
"result_url": order.result_url,
|
||||
"fail_reason": order.fail_reason.as_ref().map(|fr| fr.reason.as_str()),
|
||||
"failure_type": order.fail_reason.as_ref().map(|fr| format!("{:?}", fr.failure_type)),
|
||||
"created_at": order.created_at,
|
||||
"results": order.results.iter().map(|r| {
|
||||
serde_json::json!({
|
||||
"url": r.url,
|
||||
"width": r.width,
|
||||
"height": r.height,
|
||||
})
|
||||
}).collect::<Vec<_>>(),
|
||||
})
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod failed_job_histogram;
|
||||
pub mod find_job;
|
||||
@@ -1,24 +0,0 @@
|
||||
mod commands;
|
||||
mod utils;
|
||||
|
||||
use clap::Parser;
|
||||
use commands::run::{Cli, all_canonical_names};
|
||||
use utils::normalize_subcommands::normalize_subcommand_args;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Normalize args so underscores in subcommand names are optional.
|
||||
let canonical_names = all_canonical_names();
|
||||
let args = normalize_subcommand_args(std::env::args(), &canonical_names);
|
||||
|
||||
// Parse CLI args first so --help works without the env file.
|
||||
let cli = Cli::parse_from(args);
|
||||
|
||||
// Load secrets from .env file.
|
||||
easyenv::from_filename(".env-support-tool-secrets")
|
||||
.expect("Failed to load .env-support-tool-secrets");
|
||||
|
||||
easyenv::init_env_logger(Some("info"));
|
||||
|
||||
commands::run::run(cli).await
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod normalize_subcommands;
|
||||
@@ -1,27 +0,0 @@
|
||||
/// Normalize CLI args so that underscores in subcommand names are optional.
|
||||
///
|
||||
/// For each arg that isn't a flag (doesn't start with `-`), if removing all
|
||||
/// underscores from the arg matches removing all underscores from a canonical
|
||||
/// name, the arg is replaced with the canonical name.
|
||||
///
|
||||
/// This lets users type `findjob`, `find_job`, or even `f_ind_job` and all
|
||||
/// resolve to the canonical `find_job`.
|
||||
pub fn normalize_subcommand_args(
|
||||
args: impl IntoIterator<Item = String>,
|
||||
canonical_names: &[&str],
|
||||
) -> Vec<String> {
|
||||
args.into_iter()
|
||||
.map(|arg| {
|
||||
if arg.starts_with('-') {
|
||||
return arg;
|
||||
}
|
||||
let stripped = arg.replace('_', "").to_lowercase();
|
||||
for &name in canonical_names {
|
||||
if name.replace('_', "").to_lowercase() == stripped {
|
||||
return name.to_string();
|
||||
}
|
||||
}
|
||||
arg
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
-2
@@ -32,7 +32,5 @@ pub async fn handle_image_edit_artcraft(
|
||||
ImageEditModel::Seedream4 => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream4, GenerationModel::Seedream4).await,
|
||||
ImageEditModel::Seedream4p5 => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream4p5, GenerationModel::Seedream4p5).await,
|
||||
ImageEditModel::Seedream5Lite => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream5Lite, GenerationModel::Seedream5Lite).await,
|
||||
ImageEditModel::QwenEdit2511Angles => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::QwenEdit2511Angles, GenerationModel::QwenEdit2511Angles).await,
|
||||
ImageEditModel::Flux2LoraAngles => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Flux2LoraAngles, GenerationModel::Flux2LoraAngles).await,
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -76,9 +76,9 @@ pub(super) async fn handle_image_edit_artcraft_via_router(
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: request.horizontal_angle,
|
||||
vertical_angle: request.vertical_angle,
|
||||
zoom: request.zoom,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
|
||||
let plan = router_request.build()?;
|
||||
|
||||
-18
@@ -81,12 +81,6 @@ pub enum ImageEditModel {
|
||||
#[serde(rename = "seedream_5_lite")]
|
||||
Seedream5Lite,
|
||||
|
||||
#[serde(rename = "qwen_edit_2511_angles")]
|
||||
QwenEdit2511Angles,
|
||||
|
||||
#[serde(rename = "flux_2_lora_angles")]
|
||||
Flux2LoraAngles,
|
||||
|
||||
// #[serde(rename = "qwen")]
|
||||
// Qwen,
|
||||
//
|
||||
@@ -136,18 +130,6 @@ pub struct EnqueueEditImageCommand {
|
||||
/// Image resolution
|
||||
pub image_resolution: Option<EditImageResolution>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Only for image angle edit models.
|
||||
pub horizontal_angle: Option<f64>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Only for image angle edit models.
|
||||
pub vertical_angle: Option<f64>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Only for image angle edit models.
|
||||
pub zoom: Option<f64>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Name of the frontend caller.
|
||||
/// We'll use this to selectively trigger events.
|
||||
|
||||
@@ -13,7 +13,5 @@ pub fn image_edit_model_to_model_type(model: ImageEditModel) -> ModelType {
|
||||
ImageEditModel::Seedream4 => ModelType::Seedream4,
|
||||
ImageEditModel::Seedream4p5 => ModelType::Seedream4p5,
|
||||
ImageEditModel::Seedream5Lite => ModelType::Seedream5Lite,
|
||||
ImageEditModel::QwenEdit2511Angles => ModelType::QwenEdit2511Angles,
|
||||
ImageEditModel::Flux2LoraAngles => ModelType::Flux2LoraAngles,
|
||||
}
|
||||
}
|
||||
|
||||
-4
@@ -4,10 +4,8 @@ use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel;
|
||||
use artcraft_router::api::audio_list_ref::AudioListRef;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
use artcraft_router::api::video_list_ref::VideoListRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
|
||||
@@ -40,8 +38,6 @@ pub(super) async fn handle_artcraft_seedance_2p0(
|
||||
start_frame,
|
||||
end_frame,
|
||||
reference_images,
|
||||
reference_videos: request.reference_video_media_tokens.as_ref().map(VideoListRef::MediaFileTokens),
|
||||
reference_audio: request.reference_audio_media_tokens.as_ref().map(AudioListRef::MediaFileTokens),
|
||||
resolution: None,
|
||||
aspect_ratio: request.aspect_ratio,
|
||||
duration_seconds: request.duration_seconds,
|
||||
|
||||
-2
@@ -38,8 +38,6 @@ pub(super) async fn handle_artcraft_video_via_router(
|
||||
start_frame,
|
||||
end_frame,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
resolution: None,
|
||||
aspect_ratio: request.aspect_ratio,
|
||||
duration_seconds: request.duration_seconds,
|
||||
|
||||
-8
@@ -115,14 +115,6 @@ pub struct EnqueueImageToVideoRequest {
|
||||
/// Reference images for the video generation (e.g. character consistency, props, etc.)
|
||||
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Reference videos for the video generation (e.g. video edits, etc.)
|
||||
pub reference_video_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Reference audio for the video generation (e.g. voices, lip sync, music, etc.)
|
||||
pub reference_audio_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Optional.
|
||||
/// Text prompt used to direct the video.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
@@ -76,8 +76,6 @@ impl TaskEnqueueSuccess{
|
||||
Some(GenerationModel::Seedream4) => Some(TaskModelType::Seedream4),
|
||||
Some(GenerationModel::Seedream4p5) => Some(TaskModelType::Seedream4p5),
|
||||
Some(GenerationModel::Seedream5Lite) => Some(TaskModelType::Seedream5Lite),
|
||||
Some(GenerationModel::QwenEdit2511Angles) => Some(TaskModelType::QwenEdit2511Angles),
|
||||
Some(GenerationModel::Flux2LoraAngles) => Some(TaskModelType::Flux2LoraAngles),
|
||||
Some(GenerationModel::GrokImage) => Some(TaskModelType::GrokImage),
|
||||
Some(GenerationModel::Recraft3) => Some(TaskModelType::Recraft3),
|
||||
Some(GenerationModel::GrokVideo) => Some(TaskModelType::GrokVideo),
|
||||
|
||||
@@ -18,7 +18,6 @@ use serde_derive::{Deserialize, Serialize};
|
||||
use sqlite_tasks::queries::list_tasks_for_frontend::list_tasks_for_frontend;
|
||||
use artcraft_client::endpoints::media_files::delete_media_file::delete_media_file;
|
||||
use tauri::{AppHandle, State};
|
||||
use enums::tauri::tasks::task_failure_type::TaskFailureType;
|
||||
use enums::tauri::tasks::task_media_file_class::TaskMediaFileClass;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
@@ -32,20 +31,12 @@ pub struct GetTaskQueueCommandResponse {
|
||||
#[derive(Serialize)]
|
||||
pub struct TaskQueueItem {
|
||||
pub id: TaskId,
|
||||
|
||||
pub task_status: TaskStatus,
|
||||
pub task_type: TaskType,
|
||||
pub model_type: Option<TaskModelType>,
|
||||
|
||||
pub provider: Option<GenerationProvider>,
|
||||
pub provider_job_id: Option<String>,
|
||||
|
||||
/// If the item is done, these will be filled out.
|
||||
pub completed_item: Option<CompletedItemData>,
|
||||
|
||||
/// If the item failed, this will be filled out.
|
||||
pub failure_reason: Option<FailedItemData>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
@@ -72,12 +63,6 @@ pub struct MediaFileData {
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FailedItemData {
|
||||
pub failure_type: TaskFailureType,
|
||||
pub failure_message: Option<String>,
|
||||
}
|
||||
|
||||
impl SerializeMarker for GetTaskQueueCommandResponse {}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -118,7 +103,6 @@ pub async fn handle_request(
|
||||
|
||||
for task in tasks.tasks.into_iter() {
|
||||
let mut completed_item = None;
|
||||
let mut failure_reason = None;
|
||||
|
||||
if task.status == TaskStatus::CompleteSuccess {
|
||||
let token_and_url = task.on_complete_primary_media_file_token
|
||||
@@ -139,19 +123,6 @@ pub async fn handle_request(
|
||||
} else {
|
||||
warn!("Task {} is marked complete but has no primary media file token or URL.", task.id);
|
||||
}
|
||||
} else {
|
||||
// If either failure field is present, fill out the failure report.
|
||||
if let Some(failure_message) = task.on_failure_message.as_deref() {
|
||||
failure_reason = Some(FailedItemData {
|
||||
failure_type: task.on_failure_type.unwrap_or(TaskFailureType::Unknown),
|
||||
failure_message: Some(failure_message.to_string()),
|
||||
});
|
||||
} else if let Some(failure_type) = task.on_failure_type {
|
||||
failure_reason = Some(FailedItemData {
|
||||
failure_type,
|
||||
failure_message: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
transformed_tasks.push(TaskQueueItem {
|
||||
@@ -165,7 +136,6 @@ pub async fn handle_request(
|
||||
updated_at: task.updated_at,
|
||||
completed_at: task.completed_at,
|
||||
completed_item,
|
||||
failure_reason,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -50,11 +50,6 @@ pub enum GenerationModel {
|
||||
#[serde(rename = "seedream_5_lite")]
|
||||
Seedream5Lite,
|
||||
|
||||
#[serde(rename = "qwen_edit_2511_angles")]
|
||||
QwenEdit2511Angles,
|
||||
#[serde(rename = "flux_2_lora_angles")]
|
||||
Flux2LoraAngles,
|
||||
|
||||
#[serde(rename = "hunyuan_3d_2_0")]
|
||||
Hunyuan3d2_0,
|
||||
#[serde(rename = "hunyuan_3d_2_1")]
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::Artcraft
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use crate::services::storyteller::threads::storyteller_activity_thread::storyteller_activity_thread;
|
||||
use crate::services::storyteller::threads::storyteller_task_polling_thread::storyteller_task_polling_thread::storyteller_task_polling_thread;
|
||||
use crate::services::storyteller::threads::storyteller_task_polling_thread::storyteller_task_polling_thread;
|
||||
use errors::AnyhowResult;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
|
||||
@@ -7,4 +7,4 @@
|
||||
/// We increment this whenever we make a change to the database schema.
|
||||
/// This prevents deadlock on startup if the schema has changed from the
|
||||
/// previous version.
|
||||
pub const TASK_DATABASE_VERSION: u32 = 6;
|
||||
pub const TASK_DATABASE_VERSION: u32 = 5;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user