mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-28 04:22:51 -04:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ade8f9923 | ||
|
|
b20f73c094 | ||
|
|
82892c1aae | ||
|
|
25ef5c085c | ||
|
|
3178df86e3 | ||
|
|
a90a27e45d | ||
|
|
769faf3daa | ||
|
|
9d38b45bc2 | ||
|
|
4b5e171261 |
+14
-3
@@ -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 \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\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.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\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 \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\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.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -85,6 +85,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -94,7 +104,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -116,9 +126,10 @@
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fbe482c8987231c3b5d75f2e792d0fdd033de8a7eb821428fa034752c73edfb7"
|
||||
"hash": "2c98621c08055374fd09502c94ac40bb01d23fcea3f844cceea7446a5c0cb4dc"
|
||||
}
|
||||
+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_wallet_ledger_entry_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 maybe_debug_log_event_token = ?,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 16
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8f8b64c0af5383ff3baaa8aa2f2aa22cc760addd850d618ce543fe05816d11e0"
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n id as `id: u64`,\n event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n message,\n created_at\nFROM debug_logs\nWHERE event_token = ?\nORDER BY id ASC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 96
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "message",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 67108860
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a62b88fec40a11952cc199383090ea338f97091e8bfc6b68aad67416685889af"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO debug_logs\nSET\n event_token = ?,\n debug_log_type = ?,\n maybe_creator_user_token = ?,\n message = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a9745ecf60b564ab096ee1dbe124221b5f288a4ae38e788e19bbd359db0b580f"
|
||||
}
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
|
||||
"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 maybe_debug_log_event_token = ?,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 16
|
||||
"Right": 17
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "29916e73a1784ea0b70ef4f637520b05efccc31e01840f681e9b042f82eb8f7e"
|
||||
"hash": "c73be10c5677f78f81bc1c29872fd5af889790c3b2a8de27eb8ab64383c638d6"
|
||||
}
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n token as `token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n uuid_idempotency_token,\n job_type as `maybe_job_type`,\n product_category as `maybe_product_category`,\n inference_category,\n maybe_external_third_party,\n maybe_external_third_party_id,\n maybe_model_type,\n maybe_model_token,\n maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n maybe_inference_args,\n maybe_download_url,\n on_success_result_entity_type,\n on_success_result_entity_token,\n on_success_result_batch_token,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n maybe_creator_anonymous_visitor_token,\n creator_ip_address,\n creator_set_visibility,\n maybe_wallet_ledger_entry_token,\n maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n status,\n attempt_count as `attempt_count: u16`,\n failure_reason,\n internal_debugging_failure_reason,\n frontend_failure_category,\n maybe_routing_tag,\n assigned_worker,\n assigned_cluster,\n created_at,\n updated_at\nFROM generic_inference_jobs\nWHERE token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "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": "uuid_idempotency_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 144
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "maybe_job_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_product_category",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "inference_category",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_external_third_party",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_external_third_party_id",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 256
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_model_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_model_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_inference_args",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
"flags": "BLOB | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 262140
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_download_url",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 4096
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "on_success_result_entity_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "on_success_result_entity_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "on_success_result_batch_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "maybe_creator_anonymous_visitor_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "creator_ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 160
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "creator_set_visibility",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | BINARY | ENUM",
|
||||
"char_set": 224,
|
||||
"max_size": 28
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "maybe_wallet_ledger_entry_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "status",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
|
||||
"char_set": 224,
|
||||
"max_size": 76
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "attempt_count: u16",
|
||||
"type_info": {
|
||||
"type": "Short",
|
||||
"flags": "NOT_NULL | UNSIGNED",
|
||||
"char_set": 63,
|
||||
"max_size": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "failure_reason",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 2048
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "internal_debugging_failure_reason",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 2048
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "frontend_failure_category",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"name": "maybe_routing_tag",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 256
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "assigned_worker",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 512
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"name": "assigned_cluster",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 512
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e869f065e6f184abbabae04ecd0da6ea02c0576cc22ee55e47eb91dd34048ef1"
|
||||
}
|
||||
Generated
+5
@@ -11469,13 +11469,18 @@ name = "support-tool"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"artcraft_api_defs",
|
||||
"artcraft_client",
|
||||
"clap 4.5.60",
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
"log",
|
||||
"seedance2pro_client",
|
||||
"serde_json",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"uuid 1.22.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS debug_logs;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE debug_logs (
|
||||
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Event token. Not unique — multiple log rows can share the same event.
|
||||
event_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The type of debug log entry.
|
||||
debug_log_type VARCHAR(24) NOT NULL,
|
||||
|
||||
-- The user who created the log entry (if any).
|
||||
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- The log message body.
|
||||
message MEDIUMTEXT NOT NULL,
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
KEY index_event_token (event_token),
|
||||
KEY index_debug_log_type (debug_log_type),
|
||||
KEY index_maybe_creator_user_token (maybe_creator_user_token),
|
||||
KEY index_created_at (created_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE generic_inference_jobs
|
||||
DROP COLUMN maybe_debug_log_event_token;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE generic_inference_jobs
|
||||
ADD COLUMN maybe_debug_log_event_token VARCHAR(32) DEFAULT NULL;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE debug_logs (
|
||||
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Event token. Not unique — multiple log rows can share the same event.
|
||||
event_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The type of debug log entry.
|
||||
debug_log_type VARCHAR(24) NOT NULL,
|
||||
|
||||
-- The user who created the log entry (if any).
|
||||
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- The log message body.
|
||||
message MEDIUMTEXT NOT NULL,
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
KEY index_event_token (event_token),
|
||||
KEY index_debug_log_type (debug_log_type),
|
||||
KEY index_maybe_creator_user_token (maybe_creator_user_token),
|
||||
KEY index_created_at (created_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -204,6 +204,10 @@ CREATE TABLE generic_inference_jobs (
|
||||
-- off special workers to handle certain jobs.
|
||||
maybe_routing_tag VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- Foreign key to the debug_logs table (event_token column).
|
||||
-- Multiple debug_logs rows can share the same event token.
|
||||
maybe_debug_log_event_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== JOB SYSTEM DETAILS ==========
|
||||
|
||||
-- Jobs begin as "pending", then transition to other states.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use errors::AnyhowError;
|
||||
use reqwest::StatusCode;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
@@ -30,6 +31,12 @@ pub enum ApiError {
|
||||
backend_hostname: Option<String>,
|
||||
},
|
||||
|
||||
/// An uncategorized bad HTTP response with status code and body.
|
||||
UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
},
|
||||
|
||||
/// Cloudflare errors.
|
||||
CloudflareError(CloudflareError),
|
||||
|
||||
@@ -66,6 +73,8 @@ impl Display for ApiError {
|
||||
ApiError::TooManyRequests(msg) => write!(f, "Too many requests to Storyteller backend: {}", msg),
|
||||
ApiError::InternalServerError {body, backend_hostname} =>
|
||||
write!(f, "Internal Server Error; backend hostname: {:?} ; body: {}; ", backend_hostname, body),
|
||||
ApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body } =>
|
||||
write!(f, "Uncategorized bad response with status code {} and body: {}", status_code, body),
|
||||
// Server response handling errors
|
||||
ApiError::DeserializationError(error) => write!(f, "Deserialization error: {}", error),
|
||||
// Network errors
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use log::{info, warn};
|
||||
use reqwest::Client;
|
||||
use url::Url;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::endpoints::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse};
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
|
||||
pub struct DownloadMediaFileArgs<'a, P: AsRef<Path>> {
|
||||
pub media_token: &'a MediaFileToken,
|
||||
pub api_host: &'a ApiHost,
|
||||
pub download_path: DownloadPath<P>,
|
||||
}
|
||||
|
||||
pub enum DownloadPath<P: AsRef<Path>> {
|
||||
/// Write the file to this exact path.
|
||||
ExactFilename(P),
|
||||
/// Write the file into this directory, generating a filename from the CDN URL extension.
|
||||
Directory(P),
|
||||
}
|
||||
|
||||
pub struct DownloadMediaFileResult {
|
||||
/// The path the file was written to.
|
||||
pub downloaded_file_path: PathBuf,
|
||||
|
||||
/// The full media file response from the API.
|
||||
pub media_file_response: GetMediaFileSuccessResponse,
|
||||
|
||||
/// Size of downloaded file in bytes.
|
||||
pub filesize_bytes: usize,
|
||||
}
|
||||
|
||||
pub async fn download_media_file<P: AsRef<Path>>(
|
||||
args: DownloadMediaFileArgs<'_, P>,
|
||||
) -> Result<DownloadMediaFileResult, StorytellerError> {
|
||||
let DownloadMediaFileArgs {
|
||||
media_token,
|
||||
api_host,
|
||||
download_path
|
||||
} = args;
|
||||
|
||||
// 1. Fetch media file info from the API.
|
||||
let response = get_media_file(api_host, media_token).await?;
|
||||
let media_class = &response.media_file.media_class;
|
||||
let cdn_url = &response.media_file.media_links.cdn_url;
|
||||
|
||||
info!("Downloading media file {} of class {} from CDN: {}",
|
||||
media_token.as_str(), media_class.to_str(), cdn_url);
|
||||
|
||||
// 2. Determine the output file path.
|
||||
let output_path = match &download_path {
|
||||
DownloadPath::ExactFilename(path) => path.as_ref().to_path_buf(),
|
||||
DownloadPath::Directory(dir) => {
|
||||
let filename = derive_filename_from_url(cdn_url, &media_token);
|
||||
dir.as_ref().join(filename)
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Download the bytes from the CDN.
|
||||
let bytes = download_bytes(cdn_url).await?;
|
||||
|
||||
// 4. Write to disk.
|
||||
let mut file = fs::File::create(&output_path)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
file.write_all(&bytes)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
file.flush()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
info!("Downloaded {} bytes to {:?}", bytes.len(), output_path);
|
||||
|
||||
Ok(DownloadMediaFileResult {
|
||||
downloaded_file_path: output_path,
|
||||
media_file_response: response,
|
||||
filesize_bytes: bytes.len(),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
async fn download_bytes(url: &Url) -> Result<Vec<u8>, StorytellerError> {
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::ReqwestError(err)))?;
|
||||
|
||||
let response = client.get(url.as_str())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::OtherReqwestError(err)))?;
|
||||
|
||||
let status_code = response.status();
|
||||
|
||||
if !status_code.is_success() {
|
||||
let body = response.text().await.unwrap_or_else(|err| {
|
||||
warn!("Failed to retrieve response body: {}", err);
|
||||
"".to_string()
|
||||
});
|
||||
return Err(StorytellerError::Api(ApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code,
|
||||
body,
|
||||
}));
|
||||
}
|
||||
|
||||
let bytes = response.bytes()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Client(ClientError::ReqwestError(err)))?;
|
||||
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
/// Derive a filename from the CDN URL's path extension, falling back to the media token.
|
||||
fn derive_filename_from_url(url: &Url, media_token: &MediaFileToken) -> String {
|
||||
let path = url.path();
|
||||
let extension = Path::new(path)
|
||||
.extension() // NB: without dot '.'
|
||||
.and_then(|ext| ext.to_str())
|
||||
.unwrap_or("bin");
|
||||
|
||||
format!("{}.{}", media_token.as_str(), extension)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn derive_filename_from_url_extracts_extension() {
|
||||
let url = Url::parse("https://cdn.example.com/files/abc123.png").unwrap();
|
||||
let token = MediaFileToken::new_from_str("mf_test123");
|
||||
assert_eq!(derive_filename_from_url(&url, &token), "mf_test123.png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_filename_from_url_handles_no_extension() {
|
||||
let url = Url::parse("https://cdn.example.com/files/abc123").unwrap();
|
||||
let token = MediaFileToken::new_from_str("mf_test123");
|
||||
assert_eq!(derive_filename_from_url(&url, &token), "mf_test123.bin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_filename_from_url_handles_query_params() {
|
||||
let url = Url::parse("https://cdn.example.com/files/abc123.mp4?token=xyz").unwrap();
|
||||
let token = MediaFileToken::new_from_str("mf_test123");
|
||||
assert_eq!(derive_filename_from_url(&url, &token), "mf_test123.mp4");
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod download_media_file;
|
||||
pub mod upload_media_file_from_file;
|
||||
@@ -0,0 +1,58 @@
|
||||
# support-tool
|
||||
|
||||
Production support CLI for debugging and manual operations against external services.
|
||||
|
||||
## Building & Running
|
||||
|
||||
```bash
|
||||
cargo build -p support-tool
|
||||
cargo run -p support-tool -- seedance2pro generate_video --prompt "A corgi at the lake"
|
||||
```
|
||||
|
||||
## Environment
|
||||
|
||||
Requires a `.env-support-tool-secrets` file (or env vars) with:
|
||||
|
||||
- `SEEDANCE2PRO_COOKIES` — session cookies for Kinovi/Seedance2Pro API auth
|
||||
- `ARTCRAFT_COOKIES` — session cookies for ArtCraft API auth (format: `session=...; visitor=...`)
|
||||
|
||||
## Architecture
|
||||
|
||||
- Entry point: `src/main.rs` → normalizes args (underscores optional) → dispatches
|
||||
- Top-level commands in `src/commands/run.rs` (`TopLevelCommand` enum)
|
||||
- Each top-level command has its own module with `dispatch.rs`, `state.rs`, `subcommands/`
|
||||
- Subcommands use `#[derive(Args)]` for their argument structs
|
||||
- All subcommand `run()` functions are `async fn` returning `anyhow::Result<()>`
|
||||
|
||||
## Adding a New Subcommand (to seedance2pro)
|
||||
|
||||
1. Create `src/commands/seedance2pro/subcommands/my_command.rs`
|
||||
2. Add `pub mod my_command;` to `subcommands/mod.rs`
|
||||
3. Add the name to `SUBCOMMAND_NAMES` in `dispatch.rs`
|
||||
4. Add a variant to `Seedance2proCommand` enum in `dispatch.rs`
|
||||
5. Add a match arm in `dispatch.rs::run()`
|
||||
|
||||
## Utilities
|
||||
|
||||
- `src/utils/parse_video_model.rs` — shared parser for loose video model names → `CommonVideoModel`
|
||||
- `src/utils/normalize_subcommands.rs` — underscore-insensitive arg normalizer
|
||||
|
||||
## Conventions
|
||||
|
||||
- Use `log::info!()` for status output, never `println!` (except for final results)
|
||||
- Use `anyhow::anyhow!()` for ad-hoc errors
|
||||
- Get `Seedance2ProSession` from `state.cookies` via `from_cookies_string()`
|
||||
- Get `StorytellerCredentialSet` from `state.creds` (parsed from ARTCRAFT_COOKIES)
|
||||
- External crates used: `artcraft_client` (media file download, omni API), `seedance2pro_client` (Kinovi direct), `artcraft_api_defs` (request types)
|
||||
|
||||
## Subcommands
|
||||
|
||||
### seedance2pro (direct Kinovi API)
|
||||
|
||||
- `seedance2pro find_job --token <order_id>` — search for a job across all pages
|
||||
- `seedance2pro failed_job_histogram` — histogram of failure reasons
|
||||
- `seedance2pro generate_video --prompt <text_or_file> [--model seedance2p0] [--start_frame_media_token mf_xxx] [--localhost] [--download_path /tmp/media_files]`
|
||||
|
||||
### artcraft (omni API)
|
||||
|
||||
- `artcraft generate_video --prompt <text_or_file> [--model seedance2p0] [--start_frame_media_token mf_xxx] [--duration 5] [--localhost]`
|
||||
@@ -11,8 +11,12 @@ path = "src/main.rs"
|
||||
easyenv.workspace = true
|
||||
|
||||
# Internal
|
||||
artcraft_api_defs.workspace = true
|
||||
artcraft_client.workspace = true
|
||||
enums.workspace = true
|
||||
errors.workspace = true
|
||||
seedance2pro_client.workspace = true
|
||||
tokens.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
@@ -20,3 +24,4 @@ clap.workspace = true
|
||||
log.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
uuid = { version = "1.8.0", features = ["v4"] }
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use anyhow::anyhow;
|
||||
use clap::Subcommand;
|
||||
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
|
||||
use super::state::ArtcraftState;
|
||||
use super::subcommands;
|
||||
|
||||
/// All canonical subcommand names for this module.
|
||||
/// Used by the underscore-insensitive arg normalizer.
|
||||
pub const SUBCOMMAND_NAMES: &[&str] = &["generate_video"];
|
||||
|
||||
#[derive(Subcommand)]
|
||||
#[command(rename_all = "snake_case")]
|
||||
pub enum ArtcraftCommand {
|
||||
/// Generate a video via the ArtCraft omni endpoint
|
||||
GenerateVideo(subcommands::generate_video::GenerateVideoArgs),
|
||||
}
|
||||
|
||||
pub async fn run(command: ArtcraftCommand) -> anyhow::Result<()> {
|
||||
let cookies_str = easyenv::get_env_string_required("ARTCRAFT_COOKIES")
|
||||
.map_err(|err| anyhow!("Missing ARTCRAFT_COOKIES env var: {:?}", err))?;
|
||||
|
||||
let creds = StorytellerCredentialSet::parse_multi_cookie_header(&cookies_str)
|
||||
.map_err(|err| anyhow!("Failed to parse ARTCRAFT_COOKIES: {:?}", err))?
|
||||
.ok_or_else(|| anyhow!("ARTCRAFT_COOKIES contained no recognized cookies (expected session=... and/or visitor=...)"))?;
|
||||
|
||||
let state = ArtcraftState { creds };
|
||||
|
||||
match command {
|
||||
ArtcraftCommand::GenerateVideo(args) => subcommands::generate_video::run(&state, args).await,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod dispatch;
|
||||
mod state;
|
||||
mod subcommands;
|
||||
|
||||
pub use dispatch::ArtcraftCommand;
|
||||
pub use dispatch::run;
|
||||
@@ -0,0 +1,5 @@
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
|
||||
pub struct ArtcraftState {
|
||||
pub creds: StorytellerCredentialSet,
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use clap::Args;
|
||||
use log::info;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::utils::parse_video_model::parse_video_model;
|
||||
use super::super::state::ArtcraftState;
|
||||
|
||||
// ── Args ──
|
||||
|
||||
#[derive(Args)]
|
||||
#[command(
|
||||
override_usage = "support-tool artcraft generate_video [OPTIONS] --prompt <PROMPT>",
|
||||
after_help = "\
|
||||
EXAMPLES:
|
||||
support-tool artcraft generate_video --prompt \"A corgi at the lake\" --model seedance2
|
||||
support-tool artcraft generate_video --prompt prompt.txt --model veo3
|
||||
support-tool artcraft generate_video --prompt \"Dancing\" --start_frame_media_token mf_abc123
|
||||
support-tool artcraft generate_video --prompt \"Cat\" --image_reference_tokens \"mf_abc,mf_def\" --model happyhorse
|
||||
",
|
||||
)]
|
||||
pub struct GenerateVideoArgs {
|
||||
/// A prompt string or path to a file containing the prompt (.txt or .md).
|
||||
#[arg(long)]
|
||||
pub prompt: String,
|
||||
|
||||
/// A single media token for the start frame image.
|
||||
#[arg(long)]
|
||||
pub start_frame_media_token: Option<String>,
|
||||
|
||||
/// A single media token for the end frame image.
|
||||
#[arg(long)]
|
||||
pub end_frame_media_token: Option<String>,
|
||||
|
||||
/// Comma or space separated media tokens for image references.
|
||||
#[arg(long)]
|
||||
pub image_reference_tokens: Option<String>,
|
||||
|
||||
/// Comma or space separated media tokens for video references.
|
||||
#[arg(long)]
|
||||
pub video_reference_tokens: Option<String>,
|
||||
|
||||
/// Comma or space separated media tokens for audio references.
|
||||
#[arg(long)]
|
||||
pub audio_reference_tokens: Option<String>,
|
||||
|
||||
/// Use localhost:12345 instead of production API.
|
||||
#[arg(long)]
|
||||
pub localhost: bool,
|
||||
|
||||
/// Video model to use. Accepts canonical names (eg. "seedance_2p0") or aliases
|
||||
/// (eg. "seedance2", "happyhorse", "veo3fast"). [default: seedance2p0]
|
||||
#[arg(long)]
|
||||
pub model: Option<String>,
|
||||
|
||||
/// Duration in seconds. [default: 5]
|
||||
#[arg(long)]
|
||||
pub duration: Option<u16>,
|
||||
|
||||
/// Idempotency token (auto-generated if not specified).
|
||||
#[arg(long)]
|
||||
pub idempotency_token: Option<String>,
|
||||
}
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
pub async fn run(state: &ArtcraftState, args: GenerateVideoArgs) -> anyhow::Result<()> {
|
||||
let model_str = args.model.as_deref().unwrap_or("seedance2p0");
|
||||
let model = parse_video_model(model_str)
|
||||
.ok_or_else(|| anyhow!("Unknown model '{}'. Try --help for supported names.", model_str))?;
|
||||
|
||||
let prompt = resolve_prompt(&args.prompt)?;
|
||||
let api_host = if args.localhost { ApiHost::Localhost { port: 12345 } } else { ApiHost::Storyteller };
|
||||
let duration = args.duration.unwrap_or(5);
|
||||
|
||||
info!("Model: {:?}", model);
|
||||
info!("Prompt: {:?}", prompt);
|
||||
info!("Duration: {}s", duration);
|
||||
|
||||
// Parse media tokens.
|
||||
let start_frame_token = args.start_frame_media_token.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| MediaFileToken::new_from_str(s));
|
||||
|
||||
let end_frame_token = args.end_frame_media_token.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| MediaFileToken::new_from_str(s));
|
||||
|
||||
let image_ref_tokens = parse_media_tokens(args.image_reference_tokens.as_deref());
|
||||
let video_ref_tokens = parse_media_tokens(args.video_reference_tokens.as_deref());
|
||||
let audio_ref_tokens = parse_media_tokens(args.audio_reference_tokens.as_deref());
|
||||
|
||||
if let Some(t) = &start_frame_token { info!("Start frame: {}", t.as_str()); }
|
||||
if let Some(t) = &end_frame_token { info!("End frame: {}", t.as_str()); }
|
||||
if !image_ref_tokens.is_empty() { info!("Image refs: {:?}", image_ref_tokens.iter().map(|t| t.as_str()).collect::<Vec<_>>()); }
|
||||
if !video_ref_tokens.is_empty() { info!("Video refs: {:?}", video_ref_tokens.iter().map(|t| t.as_str()).collect::<Vec<_>>()); }
|
||||
if !audio_ref_tokens.is_empty() { info!("Audio refs: {:?}", audio_ref_tokens.iter().map(|t| t.as_str()).collect::<Vec<_>>()); }
|
||||
|
||||
// Build the omni request. Media tokens are already uploaded — no download/re-upload needed.
|
||||
let idempotency_token = args.idempotency_token
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some(idempotency_token.clone()),
|
||||
model: Some(model),
|
||||
prompt: Some(prompt),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: start_frame_token,
|
||||
end_frame_image_media_token: end_frame_token,
|
||||
reference_image_media_tokens: if image_ref_tokens.is_empty() { None } else { Some(image_ref_tokens) },
|
||||
reference_video_media_tokens: if video_ref_tokens.is_empty() { None } else { Some(video_ref_tokens) },
|
||||
reference_audio_media_tokens: if audio_ref_tokens.is_empty() { None } else { Some(audio_ref_tokens) },
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
duration_seconds: Some(duration),
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
};
|
||||
|
||||
info!("Sending request to omni endpoint (idempotency_token={})...", idempotency_token);
|
||||
|
||||
let response = omni_gen_video_generate(
|
||||
&api_host,
|
||||
Some(&state.creds),
|
||||
request,
|
||||
).await.map_err(|err| anyhow!("Omni video generation failed: {}", err))?;
|
||||
|
||||
info!("Success!");
|
||||
info!("Primary job token: {}", response.inference_job_token.as_str());
|
||||
info!("All job tokens: {:?}", response.all_job_tokens.iter().map(|t| t.as_str()).collect::<Vec<_>>());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn resolve_prompt(input: &str) -> anyhow::Result<String> {
|
||||
let looks_like_file = input.ends_with(".txt") || input.ends_with(".md");
|
||||
|
||||
if looks_like_file {
|
||||
let path = Path::new(input);
|
||||
if !path.exists() {
|
||||
return Err(anyhow!("Prompt file does not exist: {:?}", path));
|
||||
}
|
||||
let content = fs::read_to_string(path)
|
||||
.map_err(|err| anyhow!("Failed to read prompt file {:?}: {}", path, err))?;
|
||||
let trimmed = content.trim().to_string();
|
||||
info!("Read prompt from file {:?} ({} chars)", path, trimmed.len());
|
||||
Ok(trimmed)
|
||||
} else {
|
||||
info!("Using prompt string directly ({} chars)", input.len());
|
||||
Ok(input.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_media_tokens(input: Option<&str>) -> Vec<MediaFileToken> {
|
||||
let input = match input {
|
||||
Some(s) if !s.trim().is_empty() => s,
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
|
||||
input
|
||||
.split(|c: char| c == ',' || c.is_whitespace())
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| MediaFileToken::new_from_str(s))
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod generate_video;
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod artcraft;
|
||||
pub mod run;
|
||||
pub mod seedance2pro;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use super::artcraft;
|
||||
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"];
|
||||
let mut names: Vec<&str> = vec!["seedance2pro", "artcraft"];
|
||||
names.extend_from_slice(seedance2pro::dispatch::SUBCOMMAND_NAMES);
|
||||
names.extend_from_slice(artcraft::dispatch::SUBCOMMAND_NAMES);
|
||||
names
|
||||
}
|
||||
|
||||
@@ -20,11 +22,17 @@ pub struct Cli {
|
||||
#[derive(Subcommand)]
|
||||
#[command(rename_all = "snake_case")]
|
||||
pub enum TopLevelCommand {
|
||||
/// Seedance2 Pro support commands
|
||||
/// Seedance2 Pro support commands (direct Kinovi API)
|
||||
Seedance2pro {
|
||||
#[command(subcommand)]
|
||||
command: seedance2pro::Seedance2proCommand,
|
||||
},
|
||||
|
||||
/// ArtCraft support commands (omni API)
|
||||
Artcraft {
|
||||
#[command(subcommand)]
|
||||
command: artcraft::ArtcraftCommand,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run(cli: Cli) -> anyhow::Result<()> {
|
||||
@@ -32,5 +40,8 @@ pub async fn run(cli: Cli) -> anyhow::Result<()> {
|
||||
TopLevelCommand::Seedance2pro { command } => {
|
||||
seedance2pro::run(command).await
|
||||
}
|
||||
TopLevelCommand::Artcraft { command } => {
|
||||
artcraft::run(command).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ 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"];
|
||||
pub const SUBCOMMAND_NAMES: &[&str] = &[
|
||||
"failed_job_histogram",
|
||||
"find_job",
|
||||
"generate_video",
|
||||
];
|
||||
|
||||
#[derive(Subcommand)]
|
||||
#[command(rename_all = "snake_case")]
|
||||
@@ -16,6 +20,9 @@ pub enum Seedance2proCommand {
|
||||
|
||||
/// Scan all jobs and print a histogram of failure reasons
|
||||
FailedJobHistogram,
|
||||
|
||||
/// Generate a video via Seedance2Pro/Kinovi directly
|
||||
GenerateVideo(subcommands::generate_video::GenerateVideoArgs),
|
||||
}
|
||||
|
||||
pub async fn run(command: Seedance2proCommand) -> anyhow::Result<()> {
|
||||
@@ -27,5 +34,6 @@ pub async fn run(command: Seedance2proCommand) -> anyhow::Result<()> {
|
||||
match command {
|
||||
Seedance2proCommand::FindJob(args) => subcommands::find_job::run(&state, args).await,
|
||||
Seedance2proCommand::FailedJobHistogram => subcommands::failed_job_histogram::run(&state).await,
|
||||
Seedance2proCommand::GenerateVideo(args) => subcommands::generate_video::run(&state, args).await,
|
||||
}
|
||||
}
|
||||
|
||||
+475
@@ -0,0 +1,475 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use clap::Args;
|
||||
use log::info;
|
||||
|
||||
use artcraft_client::recipes::download_media_file::{
|
||||
download_media_file, DownloadMediaFileArgs, DownloadPath,
|
||||
};
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro_client::generate::video::generate_happy_horse_1p0::{
|
||||
generate_happy_horse_1p0, GenerateHappyHorse1p0Args, GenerateHappyHorse1p0Request,
|
||||
};
|
||||
use seedance2pro_client::generate::video::generate_seedance_2p0::{
|
||||
generate_seedance_2p0, GenerateSeedance2p0Args, GenerateSeedance2p0Request,
|
||||
};
|
||||
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
|
||||
generate_seedance_2p0_fast, GenerateSeedance2p0FastArgs, GenerateSeedance2p0FastRequest,
|
||||
};
|
||||
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{
|
||||
prepare_file_upload, PrepareFileUploadArgs,
|
||||
};
|
||||
use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use super::super::state::Seedance2ProState;
|
||||
|
||||
const DEFAULT_DOWNLOAD_PATH: &str = "/tmp/media_files";
|
||||
|
||||
// ── Args ──
|
||||
|
||||
#[derive(Args)]
|
||||
#[command(
|
||||
override_usage = "support-tool seedance2pro generate_video [OPTIONS] --prompt <PROMPT>",
|
||||
after_help = "\
|
||||
EXAMPLES:
|
||||
support-tool seedance2pro generate_video --prompt \"A corgi at the lake\"
|
||||
support-tool seedance2pro generate_video --prompt prompt.txt --model happyhorse
|
||||
support-tool seedance2pro generate_video --prompt \"Dancing\" --start_frame_media_token mf_abc123
|
||||
support-tool seedance2pro generate_video --prompt \"Cat\" --image_reference_tokens \"mf_abc,mf_def\"
|
||||
",
|
||||
)]
|
||||
pub struct GenerateVideoArgs {
|
||||
/// A prompt string or path to a file containing the prompt (.txt or .md).
|
||||
#[arg(long)]
|
||||
pub prompt: String,
|
||||
|
||||
/// A single media token for the start frame image.
|
||||
#[arg(long)]
|
||||
pub start_frame_media_token: Option<String>,
|
||||
|
||||
/// A single media token for the end frame image.
|
||||
#[arg(long)]
|
||||
pub end_frame_media_token: Option<String>,
|
||||
|
||||
/// Comma or space separated media tokens for image references.
|
||||
#[arg(long)]
|
||||
pub image_reference_tokens: Option<String>,
|
||||
|
||||
/// Comma or space separated media tokens for video references.
|
||||
#[arg(long)]
|
||||
pub video_reference_tokens: Option<String>,
|
||||
|
||||
/// Comma or space separated media tokens for audio references.
|
||||
#[arg(long)]
|
||||
pub audio_reference_tokens: Option<String>,
|
||||
|
||||
/// Use localhost:12345 instead of production API for artcraft_client lookups.
|
||||
#[arg(long)]
|
||||
pub localhost: bool,
|
||||
|
||||
/// Directory to cache downloaded media files. [default: /tmp/media_files]
|
||||
#[arg(long)]
|
||||
pub download_path: Option<String>,
|
||||
|
||||
/// Model: seedance2p0, seedance2p0fast, happyhorse (and underscore variants).
|
||||
/// [default: seedance2p0]
|
||||
#[arg(long)]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
// ── Model enum ──
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum VideoModel {
|
||||
Seedance2p0,
|
||||
Seedance2p0Fast,
|
||||
HappyHorse,
|
||||
}
|
||||
|
||||
fn parse_model(value: &str) -> anyhow::Result<VideoModel> {
|
||||
match value.to_lowercase().as_str() {
|
||||
"seedance2" | "seedance2p0" | "seedance_2p0" => Ok(VideoModel::Seedance2p0),
|
||||
"seedance2fast" | "seedance2p0fast" | "seedance_2p0_fast" => Ok(VideoModel::Seedance2p0Fast),
|
||||
"happyhorse" | "happy_horse" => Ok(VideoModel::HappyHorse),
|
||||
other => Err(anyhow!(
|
||||
"Unknown model '{}'. Valid: seedance2, seedance2p0, seedance_2p0, \
|
||||
seedance2fast, seedance2p0fast, seedance_2p0_fast, happyhorse, happy_horse",
|
||||
other
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resolved media inputs ──
|
||||
|
||||
struct ResolvedMediaInputs {
|
||||
start_frame_url: Option<String>,
|
||||
end_frame_url: Option<String>,
|
||||
reference_image_urls: Option<Vec<String>>,
|
||||
reference_video_urls: Option<Vec<String>>,
|
||||
reference_audio_urls: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
pub async fn run(state: &Seedance2ProState, args: GenerateVideoArgs) -> anyhow::Result<()> {
|
||||
let session = Seedance2ProSession::from_cookies_string(state.cookies.clone());
|
||||
let model = parse_model(args.model.as_deref().unwrap_or("seedance2p0"))?;
|
||||
let prompt = resolve_prompt(&args.prompt)?;
|
||||
let api_host = if args.localhost { ApiHost::Localhost { port: 12345 } } else { ApiHost::Storyteller };
|
||||
|
||||
info!("Model: {:?}", model);
|
||||
info!("Prompt: {:?}", prompt);
|
||||
|
||||
// Resolve download path.
|
||||
let download_dir = resolve_download_dir(args.download_path.as_deref())?;
|
||||
info!("Download cache directory: {:?}", download_dir);
|
||||
|
||||
// Parse media tokens.
|
||||
let start_frame_token = args.start_frame_media_token.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| MediaFileToken::new_from_str(s));
|
||||
|
||||
let end_frame_token = args.end_frame_media_token.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| MediaFileToken::new_from_str(s));
|
||||
|
||||
let image_ref_tokens = parse_media_tokens(args.image_reference_tokens.as_deref());
|
||||
let video_ref_tokens = parse_media_tokens(args.video_reference_tokens.as_deref());
|
||||
let audio_ref_tokens = parse_media_tokens(args.audio_reference_tokens.as_deref());
|
||||
|
||||
// Collect all tokens that need downloading.
|
||||
let mut all_tokens: Vec<MediaFileToken> = Vec::new();
|
||||
if let Some(t) = &start_frame_token { all_tokens.push(t.clone()); }
|
||||
if let Some(t) = &end_frame_token { all_tokens.push(t.clone()); }
|
||||
all_tokens.extend(image_ref_tokens.iter().cloned());
|
||||
all_tokens.extend(video_ref_tokens.iter().cloned());
|
||||
all_tokens.extend(audio_ref_tokens.iter().cloned());
|
||||
|
||||
info!("Total media tokens to process: {}", all_tokens.len());
|
||||
|
||||
// Download media files (with caching).
|
||||
let downloaded_files = download_media_files(&all_tokens, &api_host, &download_dir).await?;
|
||||
|
||||
// Upload media files to Kinovi/Seedance2Pro.
|
||||
let uploaded_urls = upload_media_files_to_kinovi(&session, &downloaded_files).await?;
|
||||
|
||||
// Map uploaded URLs back to their roles.
|
||||
let mut url_iter = uploaded_urls.into_iter();
|
||||
|
||||
let start_frame_url = if start_frame_token.is_some() { url_iter.next() } else { None };
|
||||
let end_frame_url = if end_frame_token.is_some() { url_iter.next() } else { None };
|
||||
|
||||
let reference_image_urls: Vec<String> = url_iter.by_ref().take(image_ref_tokens.len()).collect();
|
||||
let reference_video_urls: Vec<String> = url_iter.by_ref().take(video_ref_tokens.len()).collect();
|
||||
let reference_audio_urls: Vec<String> = url_iter.by_ref().take(audio_ref_tokens.len()).collect();
|
||||
|
||||
let media_inputs = ResolvedMediaInputs {
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls: if reference_image_urls.is_empty() { None } else { Some(reference_image_urls) },
|
||||
reference_video_urls: if reference_video_urls.is_empty() { None } else { Some(reference_video_urls) },
|
||||
reference_audio_urls: if reference_audio_urls.is_empty() { None } else { Some(reference_audio_urls) },
|
||||
};
|
||||
|
||||
// Generate video.
|
||||
generate(model, &session, &prompt, media_inputs).await
|
||||
}
|
||||
|
||||
// ── Prompt resolution ──
|
||||
|
||||
fn resolve_prompt(input: &str) -> anyhow::Result<String> {
|
||||
let looks_like_file = input.ends_with(".txt") || input.ends_with(".md");
|
||||
|
||||
if looks_like_file {
|
||||
let path = Path::new(input);
|
||||
if !path.exists() {
|
||||
return Err(anyhow!("Prompt file does not exist: {:?}", path));
|
||||
}
|
||||
let content = fs::read_to_string(path)
|
||||
.map_err(|err| anyhow!("Failed to read prompt file {:?}: {}", path, err))?;
|
||||
let trimmed = content.trim().to_string();
|
||||
info!("Read prompt from file {:?} ({} chars)", path, trimmed.len());
|
||||
Ok(trimmed)
|
||||
} else {
|
||||
info!("Using prompt string directly ({} chars)", input.len());
|
||||
Ok(input.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Download path resolution ──
|
||||
|
||||
fn resolve_download_dir(maybe_path: Option<&str>) -> anyhow::Result<PathBuf> {
|
||||
let dir = match maybe_path {
|
||||
Some(path) => {
|
||||
let p = PathBuf::from(path);
|
||||
if p.exists() && !p.is_dir() {
|
||||
return Err(anyhow!("--download_path {:?} exists but is not a directory", p));
|
||||
}
|
||||
p
|
||||
}
|
||||
None => PathBuf::from(DEFAULT_DOWNLOAD_PATH),
|
||||
};
|
||||
|
||||
if !dir.exists() {
|
||||
info!("Creating download directory: {:?}", dir);
|
||||
fs::create_dir_all(&dir)
|
||||
.map_err(|err| anyhow!("Failed to create download directory {:?}: {}", dir, err))?;
|
||||
}
|
||||
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
// ── Media token parsing ──
|
||||
|
||||
fn parse_media_tokens(input: Option<&str>) -> Vec<MediaFileToken> {
|
||||
let input = match input {
|
||||
Some(s) if !s.trim().is_empty() => s,
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
|
||||
input
|
||||
.split(|c: char| c == ',' || c.is_whitespace())
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| MediaFileToken::new_from_str(s))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ── Download media files with caching ──
|
||||
|
||||
struct DownloadedFile {
|
||||
path: PathBuf,
|
||||
extension: String,
|
||||
}
|
||||
|
||||
async fn download_media_files(
|
||||
tokens: &[MediaFileToken],
|
||||
api_host: &ApiHost,
|
||||
download_dir: &Path,
|
||||
) -> anyhow::Result<Vec<DownloadedFile>> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for token in tokens {
|
||||
// Check if file already exists in cache.
|
||||
let maybe_cached = find_cached_file(download_dir, token);
|
||||
|
||||
if let Some(cached) = maybe_cached {
|
||||
info!(
|
||||
"Cache hit: {} already exists at {:?} ({} bytes)",
|
||||
token.as_str(),
|
||||
cached.path,
|
||||
fs::metadata(&cached.path).map(|m| m.len()).unwrap_or(0),
|
||||
);
|
||||
results.push(cached);
|
||||
continue;
|
||||
}
|
||||
|
||||
info!("Downloading {} from API...", token.as_str());
|
||||
|
||||
let result = download_media_file(DownloadMediaFileArgs {
|
||||
media_token: token,
|
||||
api_host,
|
||||
download_path: DownloadPath::Directory(download_dir),
|
||||
}).await.map_err(|err| anyhow!("Failed to download {}: {}", token.as_str(), err))?;
|
||||
|
||||
let extension = result.downloaded_file_path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("bin")
|
||||
.to_string();
|
||||
|
||||
info!(
|
||||
"Downloaded: {} -> {:?} ({} bytes, class: {})",
|
||||
token.as_str(),
|
||||
result.downloaded_file_path,
|
||||
result.filesize_bytes,
|
||||
result.media_file_response.media_file.media_class.to_str(),
|
||||
);
|
||||
|
||||
results.push(DownloadedFile {
|
||||
path: result.downloaded_file_path,
|
||||
extension,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Look for an existing cached file matching the token (any extension).
|
||||
fn find_cached_file(download_dir: &Path, token: &MediaFileToken) -> Option<DownloadedFile> {
|
||||
let prefix = format!("{}.", token.as_str());
|
||||
|
||||
let entries = fs::read_dir(download_dir).ok()?;
|
||||
for entry in entries.flatten() {
|
||||
let filename = entry.file_name();
|
||||
let filename_str = filename.to_string_lossy();
|
||||
if filename_str.starts_with(&prefix) {
|
||||
let path = entry.path();
|
||||
// Skip zero-byte files — treat as incomplete downloads.
|
||||
let metadata = fs::metadata(&path).ok()?;
|
||||
if metadata.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
let extension = path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("bin")
|
||||
.to_string();
|
||||
return Some(DownloadedFile { path, extension });
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// ── Upload to Kinovi/Seedance2Pro ──
|
||||
|
||||
async fn upload_media_files_to_kinovi(
|
||||
session: &Seedance2ProSession,
|
||||
files: &[DownloadedFile],
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let mut urls = Vec::new();
|
||||
|
||||
for file in files {
|
||||
info!("Uploading {:?} to Kinovi (extension: {})...", file.path, file.extension);
|
||||
|
||||
// 1. Prepare upload (get signed URL).
|
||||
let prepare_result = prepare_file_upload(PrepareFileUploadArgs {
|
||||
session,
|
||||
extension: file.extension.clone(),
|
||||
host_override: None,
|
||||
}).await.map_err(|err| anyhow!("Failed to prepare upload for {:?}: {:?}", file.path, err))?;
|
||||
|
||||
// 2. Read file bytes.
|
||||
let file_bytes = fs::read(&file.path)
|
||||
.map_err(|err| anyhow!("Failed to read {:?}: {}", file.path, err))?;
|
||||
|
||||
info!("Uploading {} bytes to signed URL...", file_bytes.len());
|
||||
|
||||
// 3. Upload to Cloudflare R2.
|
||||
let upload_result = upload_file(UploadFileArgs {
|
||||
upload_url: prepare_result.upload_url,
|
||||
file_bytes,
|
||||
host_override: None,
|
||||
}).await.map_err(|err| anyhow!("Failed to upload {:?}: {:?}", file.path, err))?;
|
||||
|
||||
info!("Uploaded: {:?} -> {}", file.path, upload_result.public_url);
|
||||
urls.push(upload_result.public_url);
|
||||
}
|
||||
|
||||
Ok(urls)
|
||||
}
|
||||
|
||||
// ── Generate video ──
|
||||
|
||||
async fn generate(
|
||||
model: VideoModel,
|
||||
session: &Seedance2ProSession,
|
||||
prompt: &str,
|
||||
media: ResolvedMediaInputs,
|
||||
) -> anyhow::Result<()> {
|
||||
match model {
|
||||
VideoModel::HappyHorse => {
|
||||
// Happy Horse only supports start_frame_url.
|
||||
if media.end_frame_url.is_some() {
|
||||
return Err(anyhow!("Happy Horse does not support --end_frame_media_token"));
|
||||
}
|
||||
if media.reference_image_urls.is_some() {
|
||||
return Err(anyhow!("Happy Horse does not support --image_reference_tokens"));
|
||||
}
|
||||
if media.reference_video_urls.is_some() {
|
||||
return Err(anyhow!("Happy Horse does not support --video_reference_tokens"));
|
||||
}
|
||||
if media.reference_audio_urls.is_some() {
|
||||
return Err(anyhow!("Happy Horse does not support --audio_reference_tokens"));
|
||||
}
|
||||
|
||||
info!("Generating video with Happy Horse 1.0...");
|
||||
let result = generate_happy_horse_1p0(GenerateHappyHorse1p0Args {
|
||||
request: GenerateHappyHorse1p0Request {
|
||||
prompt: prompt.to_string(),
|
||||
aspect_ratio: None,
|
||||
output_resolution: None,
|
||||
batch_count: None,
|
||||
duration_seconds: 5,
|
||||
start_frame_url: media.start_frame_url,
|
||||
},
|
||||
session,
|
||||
host_override: None,
|
||||
}).await.map_err(|err| anyhow!("Happy Horse generation failed: {:?}", err))?;
|
||||
|
||||
info!("Success! task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
if let Some(task_ids) = &result.task_ids {
|
||||
info!("All task_ids: {:?}", task_ids);
|
||||
}
|
||||
if let Some(order_ids) = &result.order_ids {
|
||||
info!("All order_ids: {:?}", order_ids);
|
||||
}
|
||||
}
|
||||
VideoModel::Seedance2p0 => {
|
||||
info!("Generating video with Seedance 2.0 Pro...");
|
||||
let result = generate_seedance_2p0(GenerateSeedance2p0Args {
|
||||
request: GenerateSeedance2p0Request {
|
||||
prompt: prompt.to_string(),
|
||||
aspect_ratio: None,
|
||||
output_resolution: None,
|
||||
duration_seconds: 5,
|
||||
batch_count: None,
|
||||
start_frame_url: media.start_frame_url,
|
||||
end_frame_url: media.end_frame_url,
|
||||
reference_image_urls: media.reference_image_urls,
|
||||
reference_video_urls: media.reference_video_urls,
|
||||
reference_audio_urls: media.reference_audio_urls,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
},
|
||||
session,
|
||||
host_override: None,
|
||||
}).await.map_err(|err| anyhow!("Seedance 2.0 generation failed: {:?}", err))?;
|
||||
|
||||
info!("Success! task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
if let Some(task_ids) = &result.task_ids {
|
||||
info!("All task_ids: {:?}", task_ids);
|
||||
}
|
||||
if let Some(order_ids) = &result.order_ids {
|
||||
info!("All order_ids: {:?}", order_ids);
|
||||
}
|
||||
}
|
||||
VideoModel::Seedance2p0Fast => {
|
||||
info!("Generating video with Seedance 2.0 Fast...");
|
||||
let result = generate_seedance_2p0_fast(GenerateSeedance2p0FastArgs {
|
||||
request: GenerateSeedance2p0FastRequest {
|
||||
prompt: prompt.to_string(),
|
||||
aspect_ratio: None,
|
||||
output_resolution: None,
|
||||
duration_seconds: 5,
|
||||
batch_count: None,
|
||||
start_frame_url: media.start_frame_url,
|
||||
end_frame_url: media.end_frame_url,
|
||||
reference_image_urls: media.reference_image_urls,
|
||||
reference_video_urls: media.reference_video_urls,
|
||||
reference_audio_urls: media.reference_audio_urls,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
},
|
||||
session,
|
||||
host_override: None,
|
||||
}).await.map_err(|err| anyhow!("Seedance 2.0 Fast generation failed: {:?}", err))?;
|
||||
|
||||
info!("Success! task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
if let Some(task_ids) = &result.task_ids {
|
||||
info!("All task_ids: {:?}", task_ids);
|
||||
}
|
||||
if let Some(order_ids) = &result.order_ids {
|
||||
info!("All order_ids: {:?}", order_ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod failed_job_histogram;
|
||||
pub mod find_job;
|
||||
pub mod generate_video;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod normalize_subcommands;
|
||||
pub mod parse_video_model;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
|
||||
/// Parse a loose model name into a CommonVideoModel.
|
||||
///
|
||||
/// Accepts the canonical serde names (eg. "seedance_2p0") as well as
|
||||
/// human-friendly aliases (eg. "seedance2", "happyhorse", "veo3fast").
|
||||
pub fn parse_video_model(value: &str) -> Option<CommonVideoModel> {
|
||||
let value = value.trim();
|
||||
|
||||
// Try canonical serde deserialization first (eg. "seedance_2p0", "happy_horse_1p0").
|
||||
if let Ok(model) = serde_json::from_str::<CommonVideoModel>(&format!("\"{}\"", value)) {
|
||||
return Some(model);
|
||||
}
|
||||
|
||||
// Fallback: human-friendly aliases.
|
||||
match value.to_lowercase().replace('-', "_").as_str() {
|
||||
// Seedance family
|
||||
"seedance2" | "seedance2p0" | "seedance_2p0" => Some(CommonVideoModel::Seedance2p0),
|
||||
"seedance2fast" | "seedance2p0fast" | "seedance_2p0_fast" => Some(CommonVideoModel::Seedance2p0Fast),
|
||||
"seedance1" | "seedance10lite" | "seedance_1p0_lite" => Some(CommonVideoModel::Seedance10Lite),
|
||||
"seedance15" | "seedance1p5" | "seedance_1p5_pro" => Some(CommonVideoModel::Seedance1p5Pro),
|
||||
|
||||
// Happy Horse
|
||||
"happyhorse" | "happy_horse" | "happyhorse1" | "happy_horse_1p0" => Some(CommonVideoModel::HappyHorse1p0),
|
||||
|
||||
// Kling family
|
||||
"kling16" | "kling_1p6_pro" => Some(CommonVideoModel::Kling16Pro),
|
||||
"kling21" | "kling_2p1_pro" => Some(CommonVideoModel::Kling21Pro),
|
||||
"kling21master" | "kling_2p1_master" => Some(CommonVideoModel::Kling21Master),
|
||||
"kling25turbo" | "kling_2p5_turbo_pro" => Some(CommonVideoModel::Kling2p5TurboPro),
|
||||
"kling26" | "kling_2p6_pro" => Some(CommonVideoModel::Kling2p6Pro),
|
||||
"kling3" | "kling30" | "kling_3p0_standard" => Some(CommonVideoModel::Kling3p0Standard),
|
||||
"kling3pro" | "kling30pro" | "kling_3p0_pro" => Some(CommonVideoModel::Kling3p0Pro),
|
||||
|
||||
// Veo family
|
||||
"veo2" | "veo_2" => Some(CommonVideoModel::Veo2),
|
||||
"veo3" | "veo_3" => Some(CommonVideoModel::Veo3),
|
||||
"veo3fast" | "veo_3_fast" => Some(CommonVideoModel::Veo3Fast),
|
||||
"veo31" | "veo3p1" | "veo_3p1" => Some(CommonVideoModel::Veo3p1),
|
||||
"veo31fast" | "veo3p1fast" | "veo_3p1_fast" => Some(CommonVideoModel::Veo3p1Fast),
|
||||
|
||||
// Sora family
|
||||
"sora2" | "sora_2" => Some(CommonVideoModel::Sora2),
|
||||
"sora2pro" | "sora_2_pro" => Some(CommonVideoModel::Sora2Pro),
|
||||
|
||||
// Grok
|
||||
"grok" | "grok_video" | "grokvideo" => Some(CommonVideoModel::GrokVideo),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn canonical_names() {
|
||||
assert_eq!(parse_video_model("seedance_2p0"), Some(CommonVideoModel::Seedance2p0));
|
||||
assert_eq!(parse_video_model("happy_horse_1p0"), Some(CommonVideoModel::HappyHorse1p0));
|
||||
assert_eq!(parse_video_model("veo_3"), Some(CommonVideoModel::Veo3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aliases() {
|
||||
assert_eq!(parse_video_model("seedance2"), Some(CommonVideoModel::Seedance2p0));
|
||||
assert_eq!(parse_video_model("seedance2fast"), Some(CommonVideoModel::Seedance2p0Fast));
|
||||
assert_eq!(parse_video_model("happyhorse"), Some(CommonVideoModel::HappyHorse1p0));
|
||||
assert_eq!(parse_video_model("veo3fast"), Some(CommonVideoModel::Veo3Fast));
|
||||
assert_eq!(parse_video_model("kling3pro"), Some(CommonVideoModel::Kling3p0Pro));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown() {
|
||||
assert_eq!(parse_video_model("nonexistent"), None);
|
||||
assert_eq!(parse_video_model(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims_whitespace() {
|
||||
assert_eq!(parse_video_model(" seedance2 "), Some(CommonVideoModel::Seedance2p0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use log::warn;
|
||||
use sqlx::{Executor, MySql, QueryBuilder};
|
||||
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct BulkInsertDebugLogsArgs<'e, 'c, E>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
/// If provided, all rows will share this event token.
|
||||
/// If not provided, one will be generated.
|
||||
pub apriori_debug_log_event_token: Option<&'e DebugLogEventToken>,
|
||||
|
||||
/// The user associated with these debug log events (if any).
|
||||
pub maybe_creator_user_token: Option<&'e UserToken>,
|
||||
|
||||
/// The events to insert.
|
||||
pub events: Vec<DebugLogEvent>,
|
||||
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
/// A single debug log event to insert.
|
||||
///
|
||||
/// The `message` field should be a pre-serialized JSON string (or any text).
|
||||
/// Callers should use `serde_json::to_string(...)` before constructing this.
|
||||
pub struct DebugLogEvent {
|
||||
pub debug_log_type: DebugLogType,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Bulk-insert debug log rows. All rows share the same event token.
|
||||
///
|
||||
/// Returns the event token used. No-op (with a warning) if `events` is empty.
|
||||
pub async fn bulk_insert_debug_logs<'e, 'c: 'e, E>(
|
||||
args: BulkInsertDebugLogsArgs<'e, 'c, E>,
|
||||
) -> Result<DebugLogEventToken, sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
let event_token = match args.apriori_debug_log_event_token {
|
||||
Some(token) => token.clone(),
|
||||
None => DebugLogEventToken::generate(),
|
||||
};
|
||||
|
||||
if args.events.is_empty() {
|
||||
warn!("bulk_insert_debug_logs called with no events; no-op.");
|
||||
return Ok(event_token);
|
||||
}
|
||||
|
||||
let maybe_user_token_str = args.maybe_creator_user_token.map(|t| t.as_str().to_string());
|
||||
|
||||
let mut query_builder = QueryBuilder::new(
|
||||
"INSERT INTO debug_logs (event_token, debug_log_type, maybe_creator_user_token, message) VALUES "
|
||||
);
|
||||
|
||||
for (i, event) in args.events.iter().enumerate() {
|
||||
query_builder.push("(");
|
||||
query_builder.push_bind(event_token.as_str());
|
||||
query_builder.push(", ");
|
||||
query_builder.push_bind(event.debug_log_type.to_str());
|
||||
query_builder.push(", ");
|
||||
query_builder.push_bind(&maybe_user_token_str);
|
||||
query_builder.push(", ");
|
||||
query_builder.push_bind(&event.message);
|
||||
query_builder.push(")");
|
||||
|
||||
if i < args.events.len() - 1 {
|
||||
query_builder.push(", ");
|
||||
}
|
||||
}
|
||||
|
||||
query_builder.build()
|
||||
.execute(args.mysql_executor)
|
||||
.await?;
|
||||
|
||||
Ok(event_token)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct InsertDebugLogArgs<'e, 'c, E>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
/// If provided, the row will use this event token.
|
||||
/// If not provided, one will be generated.
|
||||
pub apriori_debug_log_event_token: Option<&'e DebugLogEventToken>,
|
||||
|
||||
/// The user associated with this debug log event (if any).
|
||||
pub maybe_creator_user_token: Option<&'e UserToken>,
|
||||
|
||||
/// The type of debug log event.
|
||||
pub debug_log_type: DebugLogType,
|
||||
|
||||
/// The log message body (pre-serialized JSON or any text).
|
||||
pub message: &'e str,
|
||||
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
/// Insert a single debug log row.
|
||||
///
|
||||
/// Returns the event token used.
|
||||
pub async fn insert_debug_log<'e, 'c: 'e, E>(
|
||||
args: InsertDebugLogArgs<'e, 'c, E>,
|
||||
) -> Result<DebugLogEventToken, sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
let event_token = match args.apriori_debug_log_event_token {
|
||||
Some(token) => token.clone(),
|
||||
None => DebugLogEventToken::generate(),
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO debug_logs
|
||||
SET
|
||||
event_token = ?,
|
||||
debug_log_type = ?,
|
||||
maybe_creator_user_token = ?,
|
||||
message = ?
|
||||
"#,
|
||||
event_token.as_str(),
|
||||
args.debug_log_type.to_str(),
|
||||
args.maybe_creator_user_token.map(|t| t.as_str()),
|
||||
args.message,
|
||||
)
|
||||
.execute(args.mysql_executor)
|
||||
.await?;
|
||||
|
||||
Ok(event_token)
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
const DEFAULT_LIMIT: u32 = 50;
|
||||
|
||||
pub struct ListDebugLogsForTokenArgs<'e, 'c, E>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
pub event_token: &'e DebugLogEventToken,
|
||||
pub limit: Option<u32>,
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DebugLogRow {
|
||||
pub id: u64,
|
||||
pub event_token: DebugLogEventToken,
|
||||
pub debug_log_type: DebugLogType,
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub message: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RawDebugLogRow {
|
||||
id: u64,
|
||||
event_token: DebugLogEventToken,
|
||||
debug_log_type: DebugLogType,
|
||||
maybe_creator_user_token: Option<UserToken>,
|
||||
message: String,
|
||||
created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
pub async fn list_debug_logs_for_token<'e, 'c: 'e, E>(
|
||||
args: ListDebugLogsForTokenArgs<'e, 'c, E>,
|
||||
) -> Result<Vec<DebugLogRow>, sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
let limit = args.limit.unwrap_or(DEFAULT_LIMIT).min(200) as i64;
|
||||
|
||||
let rows = sqlx::query_as!(
|
||||
RawDebugLogRow,
|
||||
r#"
|
||||
SELECT
|
||||
id as `id: u64`,
|
||||
event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,
|
||||
debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,
|
||||
maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
|
||||
message,
|
||||
created_at
|
||||
FROM debug_logs
|
||||
WHERE event_token = ?
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
"#,
|
||||
args.event_token.as_str(),
|
||||
limit,
|
||||
)
|
||||
.fetch_all(args.mysql_executor)
|
||||
.await?;
|
||||
|
||||
let results = rows.into_iter().map(|row| {
|
||||
DebugLogRow {
|
||||
id: row.id,
|
||||
event_token: row.event_token,
|
||||
debug_log_type: row.debug_log_type,
|
||||
maybe_creator_user_token: row.maybe_creator_user_token,
|
||||
message: row.message,
|
||||
created_at: row.created_at.and_utc(),
|
||||
}
|
||||
}).collect();
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod bulk_insert_debug_logs;
|
||||
pub mod insert_debug_log;
|
||||
pub mod list_debug_logs_for_token;
|
||||
+5
@@ -9,6 +9,7 @@ use enums::common::job_status_plus::JobStatusPlus;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
@@ -29,6 +30,7 @@ pub struct FalJobDetails {
|
||||
pub creator_ip_address: String,
|
||||
|
||||
pub maybe_prompt_token: Option<PromptToken>,
|
||||
pub maybe_debug_log_event_token: Option<DebugLogEventToken>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
@@ -51,6 +53,7 @@ struct RawJobRecord {
|
||||
creator_ip_address: String,
|
||||
|
||||
maybe_prompt_token: Option<PromptToken>,
|
||||
maybe_debug_log_event_token: Option<DebugLogEventToken>,
|
||||
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
@@ -88,6 +91,7 @@ SELECT
|
||||
jobs.creator_ip_address,
|
||||
|
||||
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
|
||||
jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,
|
||||
|
||||
jobs.created_at,
|
||||
jobs.updated_at
|
||||
@@ -129,6 +133,7 @@ fn raw_record_to_public_result(record: RawJobRecord) -> AnyhowResult<FalJobDetai
|
||||
maybe_creator_anonymous_visitor_token: record.maybe_creator_anonymous_visitor_token,
|
||||
creator_ip_address: record.creator_ip_address,
|
||||
maybe_prompt_token: record.maybe_prompt_token,
|
||||
maybe_debug_log_event_token: record.maybe_debug_log_event_token,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
})
|
||||
|
||||
+1
@@ -65,6 +65,7 @@ pub async fn insert_generic_inference_job_for_fal_queue<'e, 'c : 'e, E>(args: In
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
mysql_executor: args.mysql_executor,
|
||||
phantom: args.phantom,
|
||||
};
|
||||
|
||||
+1
@@ -68,6 +68,7 @@ pub async fn insert_generic_inference_job_for_fal_queue_mock_failure<'e, 'c: 'e,
|
||||
starting_job_status_override: Some(JobStatusPlus::CompleteFailure),
|
||||
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
|
||||
maybe_failure_reason: args.maybe_failure_reason,
|
||||
maybe_debug_log_event_token: None,
|
||||
mysql_executor: args.mysql_executor,
|
||||
phantom: args.phantom,
|
||||
};
|
||||
|
||||
+7
@@ -13,6 +13,7 @@ use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::common::visibility::Visibility;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
@@ -47,6 +48,8 @@ pub struct InsertGenericInferenceForFalWithAprioriJobTokenArgs<'e, 'c, E>
|
||||
/// Set to `Some(JobStatusPlus::CompleteFailure)` for mock/test failure jobs.
|
||||
pub starting_job_status_override: Option<JobStatusPlus>,
|
||||
|
||||
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
|
||||
|
||||
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
|
||||
pub maybe_failure_reason: Option<&'e str>,
|
||||
|
||||
@@ -139,6 +142,8 @@ SET
|
||||
is_debug_request = FALSE,
|
||||
maybe_routing_tag = NULL,
|
||||
|
||||
maybe_debug_log_event_token = ?,
|
||||
|
||||
frontend_failure_category = ?,
|
||||
failure_reason = ?,
|
||||
|
||||
@@ -164,6 +169,8 @@ SET
|
||||
args.creator_ip_address,
|
||||
args.creator_set_visibility.to_str(),
|
||||
|
||||
args.maybe_debug_log_event_token.map(|t| t.as_str()),
|
||||
|
||||
maybe_frontend_failure_category_str,
|
||||
maybe_truncated_failure_reason,
|
||||
|
||||
|
||||
+47
-4
@@ -1,5 +1,6 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::MySqlPool;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{MySql, MySqlPool};
|
||||
|
||||
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
@@ -14,10 +15,52 @@ pub struct MarkJobFailedByTokenArgs<'a> {
|
||||
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
|
||||
}
|
||||
|
||||
pub struct MarkJobFailedByTokenFromConnectionArgs<'a> {
|
||||
pub mysql_connection: &'a mut PoolConnection<MySql>,
|
||||
pub job_token: &'a InferenceJobToken,
|
||||
pub maybe_public_failure_reason: Option<&'a str>,
|
||||
pub internal_debugging_failure_reason: &'a str,
|
||||
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
|
||||
}
|
||||
|
||||
/// Permanently mark an inference job as failed, looked up by its token.
|
||||
/// Unlike `mark_generic_inference_job_failure`, this does not allow retries —
|
||||
/// the job will always land in `complete_failure`.
|
||||
/// Uses a pool to acquire a connection.
|
||||
pub async fn mark_job_failed_by_token(args: MarkJobFailedByTokenArgs<'_>) -> AnyhowResult<()> {
|
||||
execute_mark_failed(ExecuteMarkJobFailedByTokenArgs {
|
||||
executor: args.pool,
|
||||
job_token: args.job_token,
|
||||
maybe_public_failure_reason: args.maybe_public_failure_reason,
|
||||
internal_debugging_failure_reason: args.internal_debugging_failure_reason,
|
||||
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
|
||||
}).await
|
||||
}
|
||||
|
||||
/// Permanently mark an inference job as failed, looked up by its token.
|
||||
/// Uses an existing connection.
|
||||
pub async fn mark_job_failed_by_token_from_connection(args: MarkJobFailedByTokenFromConnectionArgs<'_>) -> AnyhowResult<()> {
|
||||
execute_mark_failed(ExecuteMarkJobFailedByTokenArgs {
|
||||
executor: &mut **args.mysql_connection,
|
||||
job_token: args.job_token,
|
||||
maybe_public_failure_reason: args.maybe_public_failure_reason,
|
||||
internal_debugging_failure_reason: args.internal_debugging_failure_reason,
|
||||
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
|
||||
}).await
|
||||
}
|
||||
|
||||
struct ExecuteMarkJobFailedByTokenArgs<'a, E> {
|
||||
executor: E,
|
||||
job_token: &'a InferenceJobToken,
|
||||
maybe_public_failure_reason: Option<&'a str>,
|
||||
internal_debugging_failure_reason: &'a str,
|
||||
maybe_frontend_failure_category: Option<FrontendFailureCategory>,
|
||||
}
|
||||
|
||||
async fn execute_mark_failed<'e, E>(
|
||||
args: ExecuteMarkJobFailedByTokenArgs<'_, E>,
|
||||
) -> AnyhowResult<()>
|
||||
where
|
||||
E: sqlx::Executor<'e, Database = MySql>,
|
||||
{
|
||||
let maybe_public_failure_reason = args.maybe_public_failure_reason.map(|reason| {
|
||||
let mut reason = reason.trim().to_string();
|
||||
reason.truncate(512);
|
||||
@@ -46,7 +89,7 @@ WHERE token = ?
|
||||
args.maybe_frontend_failure_category,
|
||||
args.job_token.as_str()
|
||||
)
|
||||
.execute(args.pool)
|
||||
.execute(args.executor)
|
||||
.await;
|
||||
|
||||
match query_result {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod fal;
|
||||
pub mod job;
|
||||
pub mod moderation;
|
||||
pub mod seedance2pro;
|
||||
pub mod web;
|
||||
pub mod worldlabs;
|
||||
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct GetJobByTokenForModerationArgs<'e, 'c, E>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
pub job_token: &'e InferenceJobToken,
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ModerationJobDetails {
|
||||
pub token: InferenceJobToken,
|
||||
pub uuid_idempotency_token: String,
|
||||
pub maybe_job_type: Option<String>,
|
||||
pub maybe_product_category: Option<String>,
|
||||
pub inference_category: String,
|
||||
pub maybe_external_third_party: Option<String>,
|
||||
pub maybe_external_third_party_id: Option<String>,
|
||||
pub maybe_model_type: Option<String>,
|
||||
pub maybe_model_token: Option<String>,
|
||||
pub maybe_prompt_token: Option<PromptToken>,
|
||||
pub maybe_inference_args: Option<String>,
|
||||
pub maybe_download_url: Option<String>,
|
||||
pub on_success_result_entity_type: Option<String>,
|
||||
pub on_success_result_entity_token: Option<String>,
|
||||
pub on_success_result_batch_token: Option<String>,
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub maybe_creator_anonymous_visitor_token: Option<String>,
|
||||
pub creator_ip_address: String,
|
||||
pub creator_set_visibility: String,
|
||||
pub maybe_wallet_ledger_entry_token: Option<String>,
|
||||
pub maybe_debug_log_event_token: Option<DebugLogEventToken>,
|
||||
pub status: String,
|
||||
pub attempt_count: u16,
|
||||
pub failure_reason: Option<String>,
|
||||
pub internal_debugging_failure_reason: Option<String>,
|
||||
pub frontend_failure_category: Option<String>,
|
||||
pub maybe_routing_tag: Option<String>,
|
||||
pub assigned_worker: Option<String>,
|
||||
pub assigned_cluster: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RawRow {
|
||||
token: InferenceJobToken,
|
||||
uuid_idempotency_token: String,
|
||||
maybe_job_type: Option<String>,
|
||||
maybe_product_category: Option<String>,
|
||||
inference_category: String,
|
||||
maybe_external_third_party: Option<String>,
|
||||
maybe_external_third_party_id: Option<String>,
|
||||
maybe_model_type: Option<String>,
|
||||
maybe_model_token: Option<String>,
|
||||
maybe_prompt_token: Option<PromptToken>,
|
||||
maybe_inference_args: Option<String>,
|
||||
maybe_download_url: Option<String>,
|
||||
on_success_result_entity_type: Option<String>,
|
||||
on_success_result_entity_token: Option<String>,
|
||||
on_success_result_batch_token: Option<String>,
|
||||
maybe_creator_user_token: Option<UserToken>,
|
||||
maybe_creator_anonymous_visitor_token: Option<String>,
|
||||
creator_ip_address: String,
|
||||
creator_set_visibility: String,
|
||||
maybe_wallet_ledger_entry_token: Option<String>,
|
||||
maybe_debug_log_event_token: Option<DebugLogEventToken>,
|
||||
status: String,
|
||||
attempt_count: u16,
|
||||
failure_reason: Option<String>,
|
||||
internal_debugging_failure_reason: Option<String>,
|
||||
frontend_failure_category: Option<String>,
|
||||
maybe_routing_tag: Option<String>,
|
||||
assigned_worker: Option<String>,
|
||||
assigned_cluster: Option<String>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn get_job_by_token_for_moderation<'e, 'c: 'e, E>(
|
||||
args: GetJobByTokenForModerationArgs<'e, 'c, E>,
|
||||
) -> Result<Option<ModerationJobDetails>, sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
let maybe_row = sqlx::query_as!(
|
||||
RawRow,
|
||||
r#"
|
||||
SELECT
|
||||
token as `token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,
|
||||
uuid_idempotency_token,
|
||||
job_type as `maybe_job_type`,
|
||||
product_category as `maybe_product_category`,
|
||||
inference_category,
|
||||
maybe_external_third_party,
|
||||
maybe_external_third_party_id,
|
||||
maybe_model_type,
|
||||
maybe_model_token,
|
||||
maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
|
||||
maybe_inference_args,
|
||||
maybe_download_url,
|
||||
on_success_result_entity_type,
|
||||
on_success_result_entity_token,
|
||||
on_success_result_batch_token,
|
||||
maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
|
||||
maybe_creator_anonymous_visitor_token,
|
||||
creator_ip_address,
|
||||
creator_set_visibility,
|
||||
maybe_wallet_ledger_entry_token,
|
||||
maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,
|
||||
status,
|
||||
attempt_count as `attempt_count: u16`,
|
||||
failure_reason,
|
||||
internal_debugging_failure_reason,
|
||||
frontend_failure_category,
|
||||
maybe_routing_tag,
|
||||
assigned_worker,
|
||||
assigned_cluster,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM generic_inference_jobs
|
||||
WHERE token = ?
|
||||
"#,
|
||||
args.job_token.as_str(),
|
||||
)
|
||||
.fetch_optional(args.mysql_executor)
|
||||
.await?;
|
||||
|
||||
Ok(maybe_row.map(|row| ModerationJobDetails {
|
||||
token: row.token,
|
||||
uuid_idempotency_token: row.uuid_idempotency_token,
|
||||
maybe_job_type: row.maybe_job_type,
|
||||
maybe_product_category: row.maybe_product_category,
|
||||
inference_category: row.inference_category,
|
||||
maybe_external_third_party: row.maybe_external_third_party,
|
||||
maybe_external_third_party_id: row.maybe_external_third_party_id,
|
||||
maybe_model_type: row.maybe_model_type,
|
||||
maybe_model_token: row.maybe_model_token,
|
||||
maybe_prompt_token: row.maybe_prompt_token,
|
||||
maybe_inference_args: row.maybe_inference_args,
|
||||
maybe_download_url: row.maybe_download_url,
|
||||
on_success_result_entity_type: row.on_success_result_entity_type,
|
||||
on_success_result_entity_token: row.on_success_result_entity_token,
|
||||
on_success_result_batch_token: row.on_success_result_batch_token,
|
||||
maybe_creator_user_token: row.maybe_creator_user_token,
|
||||
maybe_creator_anonymous_visitor_token: row.maybe_creator_anonymous_visitor_token,
|
||||
creator_ip_address: row.creator_ip_address,
|
||||
creator_set_visibility: row.creator_set_visibility,
|
||||
maybe_wallet_ledger_entry_token: row.maybe_wallet_ledger_entry_token,
|
||||
maybe_debug_log_event_token: row.maybe_debug_log_event_token,
|
||||
status: row.status,
|
||||
attempt_count: row.attempt_count,
|
||||
failure_reason: row.failure_reason,
|
||||
internal_debugging_failure_reason: row.internal_debugging_failure_reason,
|
||||
frontend_failure_category: row.frontend_failure_category,
|
||||
maybe_routing_tag: row.maybe_routing_tag,
|
||||
assigned_worker: row.assigned_worker,
|
||||
assigned_cluster: row.assigned_cluster,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod get_job_by_token_for_moderation;
|
||||
+7
@@ -11,6 +11,7 @@ use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::common::visibility::Visibility;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
|
||||
@@ -40,6 +41,8 @@ pub struct InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs<'e, 'c,
|
||||
pub creator_ip_address: &'e str,
|
||||
pub creator_set_visibility: Visibility,
|
||||
|
||||
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
|
||||
|
||||
pub mysql_executor: E,
|
||||
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
@@ -103,6 +106,8 @@ SET
|
||||
is_debug_request = FALSE,
|
||||
maybe_routing_tag = NULL,
|
||||
|
||||
maybe_debug_log_event_token = ?,
|
||||
|
||||
status = ?
|
||||
"#,
|
||||
args.apriori_job_token.as_str(),
|
||||
@@ -127,6 +132,8 @@ SET
|
||||
args.creator_ip_address,
|
||||
args.creator_set_visibility.to_str(),
|
||||
|
||||
args.maybe_debug_log_event_token.map(|t| t.as_str()),
|
||||
|
||||
STATUS.to_str(),
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ pub mod billing;
|
||||
pub mod browser_session_logs;
|
||||
pub mod characters;
|
||||
pub mod comments;
|
||||
pub mod debug_logs;
|
||||
pub mod database_time;
|
||||
pub mod email_sender_jobs;
|
||||
pub mod entity_stats;
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::error::enum_error::EnumError;
|
||||
|
||||
/// Maximum serialized string length for database storage.
|
||||
pub const MAX_LENGTH: usize = 24;
|
||||
|
||||
/// Used in the `debug_logs` table in a `VARCHAR(24)` field.
|
||||
///
|
||||
/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DebugLogType {
|
||||
HttpRequest,
|
||||
FalRequest,
|
||||
KinoviRequest,
|
||||
FalWebhook,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(DebugLogType);
|
||||
impl_mysql_enum_coders!(DebugLogType);
|
||||
impl_mysql_from_row!(DebugLogType);
|
||||
|
||||
impl DebugLogType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::HttpRequest => "http_request",
|
||||
Self::FalRequest => "fal_request",
|
||||
Self::KinoviRequest => "kinovi_request",
|
||||
Self::FalWebhook => "fal_webhook",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, EnumError> {
|
||||
match value {
|
||||
"http_request" => Ok(Self::HttpRequest),
|
||||
"fal_request" => Ok(Self::FalRequest),
|
||||
"kinovi_request" => Ok(Self::KinoviRequest),
|
||||
"fal_webhook" => Ok(Self::FalWebhook),
|
||||
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
BTreeSet::from([
|
||||
Self::HttpRequest,
|
||||
Self::FalRequest,
|
||||
Self::KinoviRequest,
|
||||
Self::FalWebhook,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use crate::by_table::debug_logs::debug_log_type::MAX_LENGTH;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod explicit_checks {
|
||||
use super::*;
|
||||
use crate::error::enum_error::EnumError;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(DebugLogType::HttpRequest, "http_request");
|
||||
assert_serialization(DebugLogType::FalRequest, "fal_request");
|
||||
assert_serialization(DebugLogType::KinoviRequest, "kinovi_request");
|
||||
assert_serialization(DebugLogType::FalWebhook, "fal_webhook");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(DebugLogType::HttpRequest.to_str(), "http_request");
|
||||
assert_eq!(DebugLogType::FalRequest.to_str(), "fal_request");
|
||||
assert_eq!(DebugLogType::KinoviRequest.to_str(), "kinovi_request");
|
||||
assert_eq!(DebugLogType::FalWebhook.to_str(), "fal_webhook");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(DebugLogType::from_str("http_request").unwrap(), DebugLogType::HttpRequest);
|
||||
assert_eq!(DebugLogType::from_str("fal_request").unwrap(), DebugLogType::FalRequest);
|
||||
assert_eq!(DebugLogType::from_str("kinovi_request").unwrap(), DebugLogType::KinoviRequest);
|
||||
assert_eq!(DebugLogType::from_str("fal_webhook").unwrap(), DebugLogType::FalWebhook);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_err() {
|
||||
let result = DebugLogType::from_str("invalid");
|
||||
assert!(result.is_err());
|
||||
if let Err(EnumError::CouldNotConvertFromString(value)) = result {
|
||||
assert_eq!(value, "invalid");
|
||||
} else {
|
||||
panic!("Expected EnumError::CouldNotConvertFromString");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = DebugLogType::all_variants();
|
||||
assert_eq!(variants.len(), 4);
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::HttpRequest));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::FalRequest));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::KinoviRequest));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::FalWebhook));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(DebugLogType::all_variants().len(), DebugLogType::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in DebugLogType::all_variants() {
|
||||
assert_eq!(variant, DebugLogType::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, DebugLogType::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, DebugLogType::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
for variant in DebugLogType::all_variants() {
|
||||
let serialized = variant.to_str();
|
||||
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
|
||||
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long via to_str()", variant);
|
||||
}
|
||||
for variant in DebugLogType::all_variants() {
|
||||
let json = serde_json::to_string(&variant).unwrap().replace('"', "");
|
||||
assert!(json.len() <= MAX_LENGTH, "variant {:?} is too long via JSON: {:?}", variant, json);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_names_must_only_contain_lowercase_alphanumeric_and_underscore() {
|
||||
let valid_pattern = regex::Regex::new(r"^[a-z0-9_]+$").unwrap();
|
||||
|
||||
for variant in DebugLogType::all_variants() {
|
||||
let to_str_value = variant.to_str();
|
||||
assert!(valid_pattern.is_match(to_str_value),
|
||||
"to_str() for {:?} contains invalid characters: {:?} (only a-z, 0-9, _ allowed)", variant, to_str_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod debug_log_type;
|
||||
@@ -4,6 +4,7 @@ pub mod batch_generations;
|
||||
pub mod characters;
|
||||
pub mod beta_keys;
|
||||
pub mod comments;
|
||||
pub mod debug_logs;
|
||||
pub mod email_sender_jobs;
|
||||
pub mod entity_stats;
|
||||
pub mod featured_items;
|
||||
|
||||
@@ -30,6 +30,7 @@ pub(crate) enum TokenPrefix {
|
||||
BrowserSessionLog,
|
||||
Character,
|
||||
Comment,
|
||||
DebugLogEvent,
|
||||
DownloadJob,
|
||||
EmailSenderJob,
|
||||
GoogleSignInAccount,
|
||||
@@ -116,6 +117,7 @@ impl PrefixGenerator for TokenPrefix {
|
||||
Self::BrowserSessionLog => "bsl_",
|
||||
Self::Character => "character_",
|
||||
Self::Comment => "comment_",
|
||||
Self::DebugLogEvent => "dbg_",
|
||||
Self::DownloadJob => "jdown_", // NB: Previously "JGUP:"
|
||||
Self::EmailSenderJob => "email_job_",
|
||||
Self::GoogleSignInAccount => "gsi_",
|
||||
|
||||
@@ -47,3 +47,6 @@ pub mod wallets;
|
||||
pub mod zs_voice_dataset_samples;
|
||||
pub mod zs_voice_datasets;
|
||||
pub mod zs_voices;
|
||||
|
||||
// Non-unique tokens (used as indices, not primary keys)
|
||||
pub mod non_unique;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::prefixes::TokenPrefix;
|
||||
|
||||
/// A non-unique event token for the `debug_logs` table.
|
||||
/// Multiple log rows can share the same event token.
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[cfg_attr(feature = "database", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "database", sqlx(transparent))]
|
||||
pub struct DebugLogEventToken(pub String);
|
||||
|
||||
impl_string_token!(DebugLogEventToken);
|
||||
impl_crockford_generator!(DebugLogEventToken, 32usize, TokenPrefix::DebugLogEvent, CrockfordLower);
|
||||
@@ -0,0 +1 @@
|
||||
pub mod debug_logs_event_token;
|
||||
@@ -85,6 +85,8 @@ use crate::http_server::endpoints::media_files::upsert_upload::write_engine_asse
|
||||
use crate::http_server::endpoints::media_files::upsert_upload::write_error::MediaFileWriteError;
|
||||
use crate::http_server::endpoints::media_files::upsert_upload::write_scene_file::write_scene_file_media_file_handler::*;
|
||||
use crate::http_server::endpoints::model_download::enqueue_gptsovits_model_download_handler::*;
|
||||
use crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_token_handler::*;
|
||||
use crate::http_server::endpoints::moderation::jobs::moderation_get_job_by_token_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_feature_flags::moderator_edit_user_feature_flags_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_feature_flags::moderator_list_all_available_user_feature_flags_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_feature_flags::moderator_list_user_feature_flags_handler::*;
|
||||
@@ -519,6 +521,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
crate::http_server::endpoints::moderation::wallets::moderator_add_banked_balance_to_wallet_handler::moderator_add_banked_balance_to_wallet_handler,
|
||||
crate::http_server::endpoints::moderation::wallets::moderator_create_wallet_for_user_handler::moderator_create_wallet_for_user_handler,
|
||||
crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_handler::moderator_get_wallet_handler,
|
||||
crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_token_handler::moderation_list_debug_logs_for_token_handler,
|
||||
crate::http_server::endpoints::moderation::jobs::moderation_get_job_by_token_handler::moderation_get_job_by_token_handler,
|
||||
crate::http_server::endpoints::moderation::staff_audit_logs::moderator_list_staff_audit_logs_handler::moderator_list_staff_audit_logs_handler,
|
||||
crate::http_server::endpoints::moderation::user_bans::moderation_ban_user_handler::moderation_ban_user_handler,
|
||||
crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::moderator_list_user_session_impersonation_requests_for_user_handler,
|
||||
@@ -909,6 +913,13 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
ListAllImpersonationRequestsQueryParams,
|
||||
ListAllImpersonationRequestsSuccessResponse,
|
||||
ListImpersonationRequestsPathInfo,
|
||||
GetJobByTokenPathInfo,
|
||||
GetJobByTokenSuccessResponse,
|
||||
ModerationJobResponse,
|
||||
ListDebugLogsPathInfo,
|
||||
ListDebugLogsQueryParams,
|
||||
ListDebugLogsSuccessResponse,
|
||||
DebugLogEntry,
|
||||
ListStaffAuditLogsQueryParams,
|
||||
ListStaffAuditLogsSuccessResponse,
|
||||
ListImpersonationRequestsQueryParams,
|
||||
|
||||
+1
@@ -238,6 +238,7 @@ pub async fn flux_2_lora_edit_image_angle_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -239,6 +239,7 @@ pub async fn qwen_edit_2511_edit_image_angle_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -265,6 +265,7 @@ pub async fn gemini_25_flash_edit_image_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -297,6 +297,7 @@ pub async fn gpt_image_1_edit_image_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -270,6 +270,7 @@ pub async fn qwen_edit_image_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -313,6 +313,7 @@ pub async fn bytedance_seedream_5_lite_multi_function_image_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -334,6 +334,7 @@ pub async fn bytedance_seedream_v4_multi_function_image_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -331,6 +331,7 @@ pub async fn bytedance_seedream_v4p5_multi_function_image_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -382,6 +382,7 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -343,6 +343,7 @@ pub async fn nano_banana_2_multi_function_image_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -333,6 +333,7 @@ pub async fn nano_banana_multi_function_image_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -354,6 +354,7 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -215,6 +215,7 @@ pub async fn generate_flux_pro_11_text_to_image_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -223,6 +223,7 @@ pub async fn generate_flux_pro_11_ultra_text_to_image_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -229,6 +229,7 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -266,6 +266,7 @@ pub async fn generate_kling_1_6_pro_video_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -251,6 +251,7 @@ pub async fn generate_kling_2_1_master_video_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -266,6 +266,7 @@ pub async fn generate_kling_2_1_pro_video_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -285,6 +285,7 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -261,6 +261,7 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -257,6 +257,7 @@ pub async fn generate_veo_2_image_to_video_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -263,6 +263,7 @@ pub async fn generate_veo_3_fast_image_to_video_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -265,6 +265,7 @@ pub async fn generate_veo_3_image_to_video_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -340,6 +340,7 @@ pub async fn kling_2p5_turbo_pro_multi_function_video_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -337,6 +337,7 @@ pub async fn kling_2p6_pro_multi_function_video_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -342,6 +342,7 @@ pub async fn kling_3p0_pro_multi_function_video_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -342,6 +342,7 @@ pub async fn kling_3p0_standard_multi_function_video_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -329,6 +329,7 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -422,6 +422,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
maybe_debug_log_event_token: None,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}
|
||||
|
||||
+1
@@ -358,6 +358,7 @@ pub async fn sora_2_multi_function_video_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -360,6 +360,7 @@ pub async fn sora_2_pro_multi_function_video_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -424,6 +424,7 @@ pub async fn veo_3p1_fast_multi_function_video_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -424,6 +424,7 @@ pub async fn veo_3p1_multi_function_video_gen_handler(
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
maybe_debug_log_event_token: None,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod moderation_list_debug_logs_for_token_handler;
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use mysql_queries::queries::debug_logs::list_debug_logs_for_token::{
|
||||
list_debug_logs_for_token, ListDebugLogsForTokenArgs,
|
||||
};
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{
|
||||
require_moderator, UseDatabase,
|
||||
};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
// ── Path params ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ListDebugLogsPathInfo {
|
||||
pub token: DebugLogEventToken,
|
||||
}
|
||||
|
||||
// ── Query params ──
|
||||
|
||||
#[derive(Deserialize, IntoParams, ToSchema)]
|
||||
pub struct ListDebugLogsQueryParams {
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListDebugLogsSuccessResponse {
|
||||
pub success: bool,
|
||||
pub debug_logs: Vec<DebugLogEntry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct DebugLogEntry {
|
||||
pub id: u64,
|
||||
pub event_token: DebugLogEventToken,
|
||||
pub debug_log_type: DebugLogType,
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub message: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// ── Handler ──
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Moderation",
|
||||
path = "/v1/moderation/debug_logs/list/{token}",
|
||||
params(
|
||||
("token" = String, Path, description = "Debug log event token"),
|
||||
ListDebugLogsQueryParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", body = ListDebugLogsSuccessResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn moderation_list_debug_logs_for_token_handler(
|
||||
http_request: HttpRequest,
|
||||
path: web::Path<ListDebugLogsPathInfo>,
|
||||
query: web::Query<ListDebugLogsQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ListDebugLogsSuccessResponse>, AdvancedCommonWebError> {
|
||||
let _user_session = require_moderator(
|
||||
&http_request,
|
||||
&server_state,
|
||||
UseDatabase::GrabNewConnection,
|
||||
).await.map_err(|err| {
|
||||
warn!("Moderator check failed: {:?}", err);
|
||||
AdvancedCommonWebError::NotAuthorized
|
||||
})?;
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
|
||||
|
||||
let rows = list_debug_logs_for_token(ListDebugLogsForTokenArgs {
|
||||
event_token: &path.token,
|
||||
limit: query.limit,
|
||||
mysql_executor: &mut *mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await.map_err(|err| {
|
||||
warn!("Error listing debug logs for token {}: {:?}", path.token, err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let debug_logs: Vec<DebugLogEntry> = rows.into_iter().map(|row| {
|
||||
DebugLogEntry {
|
||||
id: row.id,
|
||||
event_token: row.event_token,
|
||||
debug_log_type: row.debug_log_type,
|
||||
maybe_creator_user_token: row.maybe_creator_user_token,
|
||||
message: row.message,
|
||||
created_at: row.created_at,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
Ok(Json(ListDebugLogsSuccessResponse {
|
||||
success: true,
|
||||
debug_logs,
|
||||
}))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod get_tts_inference_queue_count;
|
||||
pub mod get_w2l_inference_queue_count;
|
||||
pub mod kill_tts_inference_jobs;
|
||||
pub mod moderation_get_job_by_token_handler;
|
||||
pub mod user;
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use serde_derive::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use mysql_queries::queries::generic_inference::moderation::get_job_by_token_for_moderation::{
|
||||
get_job_by_token_for_moderation, GetJobByTokenForModerationArgs,
|
||||
};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{
|
||||
require_moderator, UseDatabase,
|
||||
};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
// ── Path params ──
|
||||
|
||||
#[derive(serde_derive::Deserialize, ToSchema)]
|
||||
pub struct GetJobByTokenPathInfo {
|
||||
pub token: InferenceJobToken,
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct GetJobByTokenSuccessResponse {
|
||||
pub success: bool,
|
||||
pub job: ModerationJobResponse,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModerationJobResponse {
|
||||
pub token: InferenceJobToken,
|
||||
pub uuid_idempotency_token: String,
|
||||
pub maybe_job_type: Option<String>,
|
||||
pub maybe_product_category: Option<String>,
|
||||
pub inference_category: String,
|
||||
pub maybe_external_third_party: Option<String>,
|
||||
pub maybe_external_third_party_id: Option<String>,
|
||||
pub maybe_model_type: Option<String>,
|
||||
pub maybe_model_token: Option<String>,
|
||||
pub maybe_prompt_token: Option<PromptToken>,
|
||||
pub maybe_inference_args: Option<String>,
|
||||
pub maybe_download_url: Option<String>,
|
||||
pub on_success_result_entity_type: Option<String>,
|
||||
pub on_success_result_entity_token: Option<String>,
|
||||
pub on_success_result_batch_token: Option<String>,
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub maybe_creator_anonymous_visitor_token: Option<String>,
|
||||
pub creator_ip_address: String,
|
||||
pub creator_set_visibility: String,
|
||||
pub maybe_wallet_ledger_entry_token: Option<String>,
|
||||
pub maybe_debug_log_event_token: Option<DebugLogEventToken>,
|
||||
pub status: String,
|
||||
pub attempt_count: u16,
|
||||
pub failure_reason: Option<String>,
|
||||
pub internal_debugging_failure_reason: Option<String>,
|
||||
pub frontend_failure_category: Option<String>,
|
||||
pub maybe_routing_tag: Option<String>,
|
||||
pub assigned_worker: Option<String>,
|
||||
pub assigned_cluster: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// ── Handler ──
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Moderation",
|
||||
path = "/v1/moderation/job/{token}",
|
||||
params(
|
||||
("token" = String, Path, description = "Inference job token"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", body = GetJobByTokenSuccessResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Not found"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn moderation_get_job_by_token_handler(
|
||||
http_request: HttpRequest,
|
||||
path: web::Path<GetJobByTokenPathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<GetJobByTokenSuccessResponse>, AdvancedCommonWebError> {
|
||||
let _user_session = require_moderator(
|
||||
&http_request,
|
||||
&server_state,
|
||||
UseDatabase::GrabNewConnection,
|
||||
).await.map_err(|err| {
|
||||
warn!("Moderator check failed: {:?}", err);
|
||||
AdvancedCommonWebError::NotAuthorized
|
||||
})?;
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
|
||||
|
||||
let job = get_job_by_token_for_moderation(GetJobByTokenForModerationArgs {
|
||||
job_token: &path.token,
|
||||
mysql_executor: &mut *mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await.map_err(|err| {
|
||||
warn!("Error fetching job by token {}: {:?}", path.token, err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let job = job.ok_or(AdvancedCommonWebError::NotFound)?;
|
||||
|
||||
Ok(Json(GetJobByTokenSuccessResponse {
|
||||
success: true,
|
||||
job: ModerationJobResponse {
|
||||
token: job.token,
|
||||
uuid_idempotency_token: job.uuid_idempotency_token,
|
||||
maybe_job_type: job.maybe_job_type,
|
||||
maybe_product_category: job.maybe_product_category,
|
||||
inference_category: job.inference_category,
|
||||
maybe_external_third_party: job.maybe_external_third_party,
|
||||
maybe_external_third_party_id: job.maybe_external_third_party_id,
|
||||
maybe_model_type: job.maybe_model_type,
|
||||
maybe_model_token: job.maybe_model_token,
|
||||
maybe_prompt_token: job.maybe_prompt_token,
|
||||
maybe_inference_args: job.maybe_inference_args,
|
||||
maybe_download_url: job.maybe_download_url,
|
||||
on_success_result_entity_type: job.on_success_result_entity_type,
|
||||
on_success_result_entity_token: job.on_success_result_entity_token,
|
||||
on_success_result_batch_token: job.on_success_result_batch_token,
|
||||
maybe_creator_user_token: job.maybe_creator_user_token,
|
||||
maybe_creator_anonymous_visitor_token: job.maybe_creator_anonymous_visitor_token,
|
||||
creator_ip_address: job.creator_ip_address,
|
||||
creator_set_visibility: job.creator_set_visibility,
|
||||
maybe_wallet_ledger_entry_token: job.maybe_wallet_ledger_entry_token,
|
||||
maybe_debug_log_event_token: job.maybe_debug_log_event_token,
|
||||
status: job.status,
|
||||
attempt_count: job.attempt_count,
|
||||
failure_reason: job.failure_reason,
|
||||
internal_debugging_failure_reason: job.internal_debugging_failure_reason,
|
||||
frontend_failure_category: job.frontend_failure_category,
|
||||
maybe_routing_tag: job.maybe_routing_tag,
|
||||
assigned_worker: job.assigned_worker,
|
||||
assigned_cluster: job.assigned_cluster,
|
||||
created_at: job.created_at,
|
||||
updated_at: job.updated_at,
|
||||
},
|
||||
}))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod alerts;
|
||||
pub mod debug_logs;
|
||||
pub mod info;
|
||||
pub mod ip_bans;
|
||||
pub mod jobs;
|
||||
|
||||
+20
-1
@@ -12,6 +12,7 @@ use artcraft_api_defs::omni_gen::generate_response::omni_gen_image_generate_resp
|
||||
use artcraft_router::client::router_client::RouterClient;
|
||||
use artcraft_router::client::router_fal_client::RouterFalClient;
|
||||
use artcraft_router::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
@@ -19,6 +20,7 @@ use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::visibility::Visibility;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue_with_apriori_job_token::{
|
||||
insert_generic_inference_job_for_fal_queue_with_apriori_job_token,
|
||||
@@ -31,6 +33,7 @@ use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_it
|
||||
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
|
||||
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
@@ -63,6 +66,8 @@ pub async fn omni_gen_image_generate_handler(
|
||||
|
||||
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
|
||||
|
||||
let debug_log_event_token = DebugLogEventToken::generate();
|
||||
|
||||
let maybe_prompt_model_type: Option<CommonModelType> = request.model
|
||||
.as_ref()
|
||||
.map(|m| m.to_common_model_type());
|
||||
@@ -154,6 +159,19 @@ pub async fn omni_gen_image_generate_handler(
|
||||
).await?;
|
||||
}
|
||||
|
||||
// ==================== DEBUG LOG: HTTP REQUEST ==================== //
|
||||
|
||||
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
|
||||
apriori_debug_log_event_token: Some(&debug_log_event_token),
|
||||
maybe_creator_user_token: Some(user_token),
|
||||
debug_log_type: DebugLogType::HttpRequest,
|
||||
message: &serde_json::to_string(&*request).unwrap_or_default(),
|
||||
mysql_executor: &mut *mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await {
|
||||
warn!("Failed to insert HTTP request debug log: {:?}", err);
|
||||
}
|
||||
|
||||
// ==================== EXECUTE GENERATION ==================== //
|
||||
|
||||
let fal_client = RouterFalClient::new(
|
||||
@@ -236,7 +254,7 @@ pub async fn omni_gen_image_generate_handler(
|
||||
for media_token in ref_tokens {
|
||||
context_items.push(PromptContextItem {
|
||||
media_token: media_token.clone(),
|
||||
context_semantic_type: PromptContextSemanticType::Imgsrc,
|
||||
context_semantic_type: PromptContextSemanticType::Imgref,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -266,6 +284,7 @@ pub async fn omni_gen_image_generate_handler(
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
maybe_debug_log_event_token: Some(&debug_log_event_token),
|
||||
mysql_executor: &mut *transaction,
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
|
||||
+1
@@ -34,6 +34,7 @@ pub async fn insert_fal_job(args: InsertFalJobArgs<'_, '_>) -> Result<InferenceJ
|
||||
maybe_avt_token: shared.maybe_avt_token,
|
||||
creator_ip_address: shared.ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
maybe_debug_log_event_token: shared.maybe_debug_log_event_token,
|
||||
mysql_executor: &mut **shared.transaction,
|
||||
maybe_inference_args: None,
|
||||
starting_job_status_override: None,
|
||||
|
||||
+1
@@ -64,6 +64,7 @@ pub async fn insert_seedance2pro_jobs(args: InsertSeedance2proJobsArgs<'_, '_>)
|
||||
maybe_avt_token: shared.maybe_avt_token,
|
||||
creator_ip_address: shared.ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
maybe_debug_log_event_token: shared.maybe_debug_log_event_token,
|
||||
mysql_executor: &mut **shared.transaction,
|
||||
maybe_inference_args: None,
|
||||
phantom: Default::default(),
|
||||
|
||||
+2
@@ -1,5 +1,6 @@
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
@@ -9,6 +10,7 @@ pub struct SharedJobArgs<'a, 'tx> {
|
||||
pub user_token: &'a UserToken,
|
||||
pub maybe_avt_token: Option<&'a AnonymousVisitorTrackingToken>,
|
||||
pub maybe_prompt_token: Option<&'a PromptToken>,
|
||||
pub maybe_debug_log_event_token: Option<&'a DebugLogEventToken>,
|
||||
pub ip_address: &'a str,
|
||||
pub transaction: &'a mut sqlx::Transaction<'tx, sqlx::MySql>,
|
||||
}
|
||||
|
||||
+20
@@ -10,6 +10,7 @@ use url::Url;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::generate_response::omni_gen_video_generate_response::OmniGenVideoGenerateResponse;
|
||||
use artcraft_router::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
@@ -17,6 +18,7 @@ use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{
|
||||
insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem,
|
||||
@@ -25,6 +27,7 @@ use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPrompt
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
|
||||
@@ -64,6 +67,8 @@ pub async fn omni_gen_video_generate_handler(
|
||||
|
||||
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
|
||||
|
||||
let debug_log_event_token = DebugLogEventToken::generate();
|
||||
|
||||
let maybe_prompt_model_type: Option<CommonModelType> = request.model
|
||||
.as_ref()
|
||||
.map(|m| m.to_common_model_type());
|
||||
@@ -207,6 +212,19 @@ pub async fn omni_gen_video_generate_handler(
|
||||
}).await?
|
||||
};
|
||||
|
||||
// ==================== DEBUG LOG: HTTP REQUEST ==================== //
|
||||
|
||||
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
|
||||
apriori_debug_log_event_token: Some(&debug_log_event_token),
|
||||
maybe_creator_user_token: Some(user_token),
|
||||
debug_log_type: DebugLogType::HttpRequest,
|
||||
message: &serde_json::to_string(&*request).unwrap_or_default(),
|
||||
mysql_executor: &mut *mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await {
|
||||
warn!("Failed to insert HTTP request debug log: {:?}", err);
|
||||
}
|
||||
|
||||
// ==================== WRITE RESULT ==================== //
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
@@ -304,6 +322,7 @@ pub async fn omni_gen_video_generate_handler(
|
||||
user_token,
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
maybe_prompt_token: prompt_token.as_ref(),
|
||||
maybe_debug_log_event_token: Some(&debug_log_event_token),
|
||||
ip_address: &ip_address,
|
||||
transaction: &mut transaction,
|
||||
},
|
||||
@@ -324,6 +343,7 @@ pub async fn omni_gen_video_generate_handler(
|
||||
user_token,
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
maybe_prompt_token: prompt_token.as_ref(),
|
||||
maybe_debug_log_event_token: Some(&debug_log_event_token),
|
||||
ip_address: &ip_address,
|
||||
transaction: &mut transaction,
|
||||
},
|
||||
|
||||
+6
-1
@@ -53,17 +53,22 @@ pub async fn fal_webhook_handler(
|
||||
// Step 3: Hydrate the webhook contents.
|
||||
let hydrated_contents = hydrate_webhook_contents(&webhook_payload);
|
||||
|
||||
// Step 3.5: Acquire a MySQL connection for the handlers.
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
|
||||
|
||||
// Step 4 & 5: Branch on the inner payload type.
|
||||
let result = match hydrated_contents {
|
||||
HydratedWebhookContents::Success(success_data) => {
|
||||
handle_successful_fal_webhook(&server_state, request_id, &success_data, &server_state.pager).await
|
||||
handle_successful_fal_webhook(&server_state, &mut mysql_connection, request_id, &success_data, &raw_body, &server_state.pager).await
|
||||
}
|
||||
HydratedWebhookContents::Error(error_data) => {
|
||||
handle_failed_fal_webhook(
|
||||
&server_state,
|
||||
&mut mysql_connection,
|
||||
request_id,
|
||||
&error_data,
|
||||
webhook_payload.error.as_deref(),
|
||||
&raw_body,
|
||||
).await
|
||||
}
|
||||
HydratedWebhookContents::PayloadError(payload_error_data) => {
|
||||
|
||||
+25
-5
@@ -1,22 +1,28 @@
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::state::server_state::ServerState;
|
||||
use actix_web::web::Json;
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
|
||||
use fal_client::webhook_api::raw::webhook_error_type::WebhookErrorType;
|
||||
use fal_client::webhook_api::hydrated::hydrated_webhook_contents::ErrorData;
|
||||
use http_server_common::response::response_success_helpers::SimpleGenericJsonSuccess;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::get_inference_job_by_fal_id;
|
||||
use mysql_queries::queries::generic_inference::job::mark_job_failed_by_token::{mark_job_failed_by_token, MarkJobFailedByTokenArgs};
|
||||
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::get_inference_job_by_fal_id_from_connection;
|
||||
use mysql_queries::queries::generic_inference::job::mark_job_failed_by_token::{mark_job_failed_by_token_from_connection, MarkJobFailedByTokenFromConnectionArgs};
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::MySql;
|
||||
|
||||
/// Handle a FAL webhook with status ERROR.
|
||||
///
|
||||
/// Looks up the job by request_id and marks it as failed using the parsed error data.
|
||||
pub async fn handle_failed_fal_webhook(
|
||||
server_state: &ServerState,
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
request_id: &str,
|
||||
error_data: &ErrorData,
|
||||
maybe_top_level_error: Option<&str>,
|
||||
raw_body: &str,
|
||||
) -> Result<Json<SimpleGenericJsonSuccess>, AdvancedCommonWebError> {
|
||||
|
||||
info!(
|
||||
@@ -28,7 +34,7 @@ pub async fn handle_failed_fal_webhook(
|
||||
);
|
||||
|
||||
// Look up the job record.
|
||||
let job = match get_inference_job_by_fal_id(request_id, &server_state.mysql_pool).await {
|
||||
let job = match get_inference_job_by_fal_id_from_connection(request_id, mysql_connection).await {
|
||||
Ok(Some(record)) => record,
|
||||
Ok(None) => {
|
||||
warn!("Could not find job record by fal request_id: {}", request_id);
|
||||
@@ -40,6 +46,20 @@ pub async fn handle_failed_fal_webhook(
|
||||
}
|
||||
};
|
||||
|
||||
// Insert debug log for the webhook payload.
|
||||
if let Some(debug_log_event_token) = &job.maybe_debug_log_event_token {
|
||||
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
|
||||
apriori_debug_log_event_token: Some(debug_log_event_token),
|
||||
maybe_creator_user_token: job.maybe_creator_user_token.as_ref(),
|
||||
debug_log_type: DebugLogType::FalWebhook,
|
||||
message: raw_body,
|
||||
mysql_executor: &mut **mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await {
|
||||
warn!("Failed to insert Fal webhook debug log: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Build a failure reason from the error data or the top-level error string.
|
||||
let public_failure_reason = if let Some(msg) = &error_data.message {
|
||||
msg.clone()
|
||||
@@ -67,8 +87,8 @@ pub async fn handle_failed_fal_webhook(
|
||||
public_failure_reason,
|
||||
);
|
||||
|
||||
if let Err(err) = mark_job_failed_by_token(MarkJobFailedByTokenArgs {
|
||||
pool: &server_state.mysql_pool,
|
||||
if let Err(err) = mark_job_failed_by_token_from_connection(MarkJobFailedByTokenFromConnectionArgs {
|
||||
mysql_connection,
|
||||
job_token: &job.job_token,
|
||||
maybe_public_failure_reason: Some(&public_failure_reason),
|
||||
internal_debugging_failure_reason: &internal_failure_reason,
|
||||
|
||||
+24
-4
@@ -1,10 +1,14 @@
|
||||
use actix_web::web::Json;
|
||||
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
|
||||
use fal_client::webhook_api::hydrated::hydrated_webhook_contents::WebhookSuccessData;
|
||||
use http_server_common::response::response_success_helpers::SimpleGenericJsonSuccess;
|
||||
use log::{info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::get_inference_job_by_fal_id;
|
||||
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::get_inference_job_by_fal_id_from_connection;
|
||||
use mysql_queries::queries::generic_inference::fal::mark_fal_generic_inference_job_successfully_done::{mark_fal_generic_inference_job_successfully_done, MarkJobArgs};
|
||||
use pager::client::pager::Pager;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::MySql;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
@@ -16,14 +20,16 @@ use super::process_video_payload::process_video_payload;
|
||||
|
||||
pub async fn handle_successful_fal_webhook(
|
||||
server_state: &ServerState,
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
request_id: &str,
|
||||
success_data: &WebhookSuccessData,
|
||||
raw_body: &str,
|
||||
pager: &Pager,
|
||||
) -> Result<Json<SimpleGenericJsonSuccess>, AdvancedCommonWebError> {
|
||||
|
||||
let db_result = get_inference_job_by_fal_id(
|
||||
let db_result = get_inference_job_by_fal_id_from_connection(
|
||||
request_id,
|
||||
&server_state.mysql_pool,
|
||||
mysql_connection,
|
||||
).await;
|
||||
|
||||
let job = match db_result {
|
||||
@@ -40,6 +46,20 @@ pub async fn handle_successful_fal_webhook(
|
||||
|
||||
info!("Fal webhook job record for request_id {}: {:?}", request_id, job);
|
||||
|
||||
// Insert debug log for the webhook payload.
|
||||
if let Some(debug_log_event_token) = &job.maybe_debug_log_event_token {
|
||||
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
|
||||
apriori_debug_log_event_token: Some(debug_log_event_token),
|
||||
maybe_creator_user_token: job.maybe_creator_user_token.as_ref(),
|
||||
debug_log_type: DebugLogType::FalWebhook,
|
||||
message: raw_body,
|
||||
mysql_executor: &mut **mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await {
|
||||
warn!("Failed to insert Fal webhook debug log: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let mut maybe_media_token = None;
|
||||
let mut maybe_batch_token = None;
|
||||
|
||||
@@ -92,7 +112,7 @@ pub async fn handle_successful_fal_webhook(
|
||||
job_token: &job.job_token,
|
||||
media_file_token: &media_token,
|
||||
maybe_batch_token: maybe_batch_token.as_ref(),
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
mysql_executor: &mut **mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await.map_err(|err| {
|
||||
warn!("Error marking job as successfully done for request_id {}: {:?}", request_id, err);
|
||||
|
||||
+12
@@ -36,6 +36,8 @@ use crate::http_server::endpoints::moderation::user::moderator_user_lookup_by_st
|
||||
use crate::http_server::endpoints::moderation::user::moderator_user_lookup_handler::moderator_user_lookup_handler;
|
||||
use crate::http_server::endpoints::moderation::alerts::moderation_send_alert_handler::moderation_send_alert_handler;
|
||||
use crate::http_server::endpoints::moderation::staff_audit_logs::moderator_list_staff_audit_logs_handler::moderator_list_staff_audit_logs_handler;
|
||||
use crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_token_handler::moderation_list_debug_logs_for_token_handler;
|
||||
use crate::http_server::endpoints::moderation::jobs::moderation_get_job_by_token_handler::moderation_get_job_by_token_handler;
|
||||
use crate::http_server::endpoints::moderation::user_feature_flags::moderator_edit_user_feature_flags_handler::moderator_edit_user_feature_flags_handler;
|
||||
use crate::http_server::endpoints::moderation::user_feature_flags::moderator_list_all_available_user_feature_flags_handler::moderator_list_all_available_user_feature_flags_handler;
|
||||
use crate::http_server::endpoints::moderation::user_feature_flags::moderator_list_user_feature_flags_handler::moderator_list_user_feature_flags_handler;
|
||||
@@ -61,6 +63,12 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
.service(web::scope("/debug_logs")
|
||||
.service(web::resource("/list/{token}")
|
||||
.route(web::get().to(moderation_list_debug_logs_for_token_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
.service(web::resource("/user_feature_flags/list")
|
||||
.route(web::get().to(moderator_list_all_available_user_feature_flags_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
@@ -88,6 +96,10 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
.service(web::resource("/job/{token}")
|
||||
.route(web::get().to(moderation_get_job_by_token_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::scope("/jobs")
|
||||
.service(web::resource("/user/{user_token}/list")
|
||||
.route(web::get().to(list_user_jobs_handler))
|
||||
|
||||
Generated
+85
-168
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user