Compare commits

...
149 changed files with 3473 additions and 7246 deletions
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO debug_logs\nSET\n event_token = ?,\n debug_log_type = ?,\n maybe_log_level = ?,\n maybe_creator_user_token = ?,\n maybe_ip_address = ?,\n maybe_url = ?,\n message = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "32a8d1cfec89ba2056463a33ddc501a8a99283c7c00982a7fa79f7ff382c9b22"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO voice_clone_requests\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n maybe_user_token = ?,\n email_address = ?,\n discord_username = ?,\n\n is_for_private_use = ?,\n is_for_public_use = ?,\n\n is_for_studio = ?,\n is_for_twitch_tts = ?,\n is_for_api_use = ?,\n is_for_music = ?,\n is_for_games = ?,\n is_for_other = ?,\n optional_notes_on_use = ?,\n\n is_own_voice = ?,\n is_third_party_voice = ?,\n optional_notes_on_subject = ?,\n\n has_clean_audio_recordings = ?,\n has_good_microphone = ?,\n\n optional_questions = ?,\n optional_extra_comments = ?,\n\n ip_address_creation = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 22
},
"nullable": []
},
"hash": "4cea10944b8869419ecdfea30c58f49a87c090b9083078043177c33737ecb949"
}
@@ -1,36 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n COUNT(distinct token) as record_count,\n NOW() as present_time\nFROM\n(\n SELECT token\n FROM tts_inference_jobs\n WHERE status = \"started\"\n AND created_at > (CURDATE() - INTERVAL 5 MINUTE)\nUNION\n SELECT token\n FROM tts_inference_jobs\n WHERE status IN (\"pending\", \"attempt_failed\")\n) as t\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "record_count",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 1,
"name": "present_time",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false
]
},
"hash": "6098826a8ada75442be5b7ac1d0199694e01c5959666f59986cd8b2ed6f51882"
}
@@ -1,256 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token,\n\n maybe_user_token,\n email_address,\n discord_username,\n\n is_for_private_use,\n is_for_public_use,\n\n is_for_studio,\n is_for_twitch_tts,\n is_for_api_use,\n is_for_music,\n is_for_games,\n is_for_other,\n optional_notes_on_use,\n\n is_own_voice,\n is_third_party_voice,\n optional_notes_on_subject,\n\n has_clean_audio_recordings,\n has_good_microphone,\n\n optional_questions,\n optional_extra_comments,\n\n created_at,\n updated_at\nFROM voice_clone_requests\nWHERE\n token = ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "maybe_user_token",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "email_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "discord_username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 4,
"name": "is_for_private_use",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 5,
"name": "is_for_public_use",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 6,
"name": "is_for_studio",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 7,
"name": "is_for_twitch_tts",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 8,
"name": "is_for_api_use",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 9,
"name": "is_for_music",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 10,
"name": "is_for_games",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 11,
"name": "is_for_other",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "optional_notes_on_use",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 13,
"name": "is_own_voice",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 14,
"name": "is_third_party_voice",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 15,
"name": "optional_notes_on_subject",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 16,
"name": "has_clean_audio_recordings",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 17,
"name": "has_good_microphone",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 18,
"name": "optional_questions",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 19,
"name": "optional_extra_comments",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 21,
"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,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
true,
false,
false,
true,
true,
false,
false
]
},
"hash": "922a7dbcfa24a2c5901d0453c229b066b3b4eea2c73fa533651bb27365f17be0"
}
@@ -1,80 +0,0 @@
{
"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"
}
@@ -1,12 +0,0 @@
{
"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"
}
@@ -0,0 +1,146 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n d.id as `id: u64`,\n d.event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n d.debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,\n d.maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`,\n d.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n d.maybe_ip_address,\n d.maybe_url,\n d.message,\n d.created_at,\n u.display_name as `maybe_user_display_name?`,\n u.username as `maybe_user_username?`,\n u.email_gravatar_hash as `maybe_user_gravatar_hash?`\nFROM debug_logs d\nLEFT OUTER JOIN users u\n ON u.token = d.maybe_creator_user_token\nWHERE d.maybe_creator_user_token = ?\n AND d.id < ?\nORDER BY d.id DESC\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_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_ip_address",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 6,
"name": "maybe_url",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 7,
"name": "message",
"type_info": {
"type": "Blob",
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 8,
"name": "created_at",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 9,
"name": "maybe_user_display_name?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 10,
"name": "maybe_user_username?",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 11,
"name": "maybe_user_gravatar_hash?",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
false,
false,
true,
true,
true
]
},
"hash": "d1ade7fcfd3efac1cd3163c392110327bb73edb74fa0d9693fee5feddb8a3223"
}
@@ -1,256 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token,\n\n maybe_user_token,\n email_address,\n discord_username,\n\n is_for_private_use,\n is_for_public_use,\n\n is_for_studio,\n is_for_twitch_tts,\n is_for_api_use,\n is_for_music,\n is_for_games,\n is_for_other,\n optional_notes_on_use,\n\n is_own_voice,\n is_third_party_voice,\n optional_notes_on_subject,\n\n has_clean_audio_recordings,\n has_good_microphone,\n\n optional_questions,\n optional_extra_comments,\n\n created_at,\n updated_at\nFROM voice_clone_requests\nWHERE\n maybe_user_token = ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "maybe_user_token",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "email_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "discord_username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 4,
"name": "is_for_private_use",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 5,
"name": "is_for_public_use",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 6,
"name": "is_for_studio",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 7,
"name": "is_for_twitch_tts",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 8,
"name": "is_for_api_use",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 9,
"name": "is_for_music",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 10,
"name": "is_for_games",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 11,
"name": "is_for_other",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "optional_notes_on_use",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 13,
"name": "is_own_voice",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 14,
"name": "is_third_party_voice",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 15,
"name": "optional_notes_on_subject",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 16,
"name": "has_clean_audio_recordings",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 17,
"name": "has_good_microphone",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 18,
"name": "optional_questions",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 19,
"name": "optional_extra_comments",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 21,
"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,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
true,
false,
false,
true,
true,
false,
false
]
},
"hash": "e5ef2bda28221a570da6dcfa73e76cae9910a0b4ee53005a87176ecf1be73dd7"
}
@@ -0,0 +1,146 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n d.id as `id: u64`,\n d.event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n d.debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,\n d.maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`,\n d.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n d.maybe_ip_address,\n d.maybe_url,\n d.message,\n d.created_at,\n u.display_name as `maybe_user_display_name?`,\n u.username as `maybe_user_username?`,\n u.email_gravatar_hash as `maybe_user_gravatar_hash?`\nFROM debug_logs d\nLEFT OUTER JOIN users u\n ON u.token = d.maybe_creator_user_token\nWHERE d.event_token = ?\nORDER BY d.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_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_ip_address",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 6,
"name": "maybe_url",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 7,
"name": "message",
"type_info": {
"type": "Blob",
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 8,
"name": "created_at",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 9,
"name": "maybe_user_display_name?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 10,
"name": "maybe_user_username?",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 11,
"name": "maybe_user_gravatar_hash?",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
false,
false,
true,
true,
true
]
},
"hash": "e9a149ffc963a59019fdd69bd53128fad946856d432108bf9b923dfeaf2442c5"
}
Generated
+15
View File
@@ -3621,6 +3621,7 @@ dependencies = [
"env_logger 0.11.9",
"log",
"storyteller_root",
"trace_id",
"workspace-build-acceleration",
]
@@ -6766,6 +6767,7 @@ version = "0.0.1"
dependencies = [
"env_logger 0.11.9",
"log",
"trace_id",
"workspace-build-acceleration",
]
@@ -8286,6 +8288,7 @@ dependencies = [
"rootly_client",
"sha2 0.11.0-rc.5",
"tokio",
"trace_id",
"workspace-build-acceleration",
]
@@ -11678,6 +11681,7 @@ dependencies = [
"tokens",
"tokio",
"toml 1.0.7+spec-1.1.0",
"trace_id",
"tts_common",
"url",
"url_config",
@@ -13316,6 +13320,17 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "trace_id"
version = "0.0.1"
dependencies = [
"rand 0.8.5",
"serde",
"serde_derive",
"tokio",
"workspace-build-acceleration",
]
[[package]]
name = "tracing"
version = "0.1.41"
+2
View File
@@ -130,6 +130,7 @@ members = [
"crates/schema/public/enums",
"crates/schema/public/reusable_types", # TODO(bt, 2022-12-21): Deprecate
"crates/schema/public/tokens",
"crates/schema/trace_id",
# Services: API servers, jobs, workers, etc.
"crates/service/job/gmicloud_job",
@@ -293,6 +294,7 @@ subprocess_common = { path = "crates/lib/deprecated/subprocess_common" }
test_data = { path = "crates/testing/test_data" }
test_utils = { path = "crates/testing/test_utils" }
tokens = { path = "crates/schema/public/tokens" }
trace_id = { path = "crates/schema/trace_id" }
url_utils = { path = "crates/lib/url_utils" }
users = { path = "crates/lib/users" }
uuid_utils = { path = "crates/lib/uuid_utils" }
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
DROP COLUMN maybe_log_level;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
ADD COLUMN maybe_log_level VARCHAR(16) DEFAULT NULL;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
DROP INDEX index_maybe_log_level;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
ADD INDEX index_maybe_log_level (maybe_log_level);
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
DROP COLUMN maybe_ip_address;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
ADD COLUMN maybe_ip_address VARCHAR(40) DEFAULT NULL;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
DROP COLUMN maybe_url;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
ADD COLUMN maybe_url VARCHAR(255) DEFAULT NULL;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE media_files
DROP INDEX idx_creator_created_at,
ALGORITHM=INPLACE, LOCK=NONE;
@@ -0,0 +1,17 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Composite index for the library / user profile list queries:
-- WHERE maybe_creator_user_token = ? ... ORDER BY created_at DESC LIMIT n
--
-- Without this, the optimizer picks an index-merge intersection against huge
-- low-selectivity indexes (e.g. index_user_deleted_at, ~4.75M NULL entries),
-- scanning millions of index records per page load.
--
-- NB: ADD INDEX is online DDL (ALGORITHM=INPLACE, LOCK=NONE) — reads and writes
-- continue during the build. Expect a few minutes of elevated I/O on a table
-- this size (~8.5M rows).
ALTER TABLE media_files
ADD INDEX idx_creator_created_at (maybe_creator_user_token, created_at),
ALGORITHM=INPLACE, LOCK=NONE;
@@ -13,9 +13,18 @@ CREATE TABLE debug_logs (
-- The type of debug log entry.
debug_log_type VARCHAR(24) NOT NULL,
-- The severity level of the log entry (info, warn, error, debug, trace).
maybe_log_level VARCHAR(16) DEFAULT NULL,
-- The user who created the log entry (if any).
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
-- The client IP address of the request (if any). Wide enough for IPv6.
maybe_ip_address VARCHAR(40) DEFAULT NULL,
-- The request URL (if any). Truncated to 255 characters on insert.
maybe_url VARCHAR(255) DEFAULT NULL,
-- The log message body.
message MEDIUMTEXT NOT NULL,
@@ -25,6 +34,7 @@ CREATE TABLE debug_logs (
PRIMARY KEY (id),
KEY index_event_token (event_token),
KEY index_debug_log_type (debug_log_type),
KEY index_maybe_log_level (maybe_log_level),
KEY index_maybe_creator_user_token (maybe_creator_user_token),
KEY index_created_at (created_at)
@@ -364,6 +364,7 @@ CREATE TABLE media_files (
KEY index_user_deleted_at (user_deleted_at),
KEY index_mod_deleted_at (mod_deleted_at),
KEY index_media_files_created_at (created_at),
KEY index_media_files_updated_at (updated_at)
KEY index_media_files_updated_at (updated_at),
KEY idx_creator_created_at (maybe_creator_user_token, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,36 @@
use chrono::{DateTime, Utc};
use serde_derive::Serialize;
use utoipa::ToSchema;
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
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;
/// A single debug log row.
/// Shared by the moderation debug-log list endpoints.
#[derive(Serialize, ToSchema)]
pub struct ModerationDebugLogEntry {
pub id: u64,
pub event_token: DebugLogEventToken,
pub debug_log_type: DebugLogType,
pub maybe_log_level: Option<DebugLogLevel>,
pub maybe_creator_user_token: Option<UserToken>,
/// The client IP address of the request (if recorded).
pub maybe_ip_address: Option<String>,
/// The request URL (if recorded; truncated to 255 chars).
pub maybe_url: Option<String>,
pub message: String,
pub created_at: DateTime<Utc>,
/// The creator user, joined in when the row has a user token.
pub maybe_user: Option<ModerationDebugLogUser>,
}
/// The creator user of a debug log row.
#[derive(Serialize, ToSchema)]
pub struct ModerationDebugLogUser {
pub user_token: UserToken,
pub display_name: String,
pub username: String,
pub gravatar_hash: String,
}
@@ -0,0 +1,4 @@
pub mod debug_log_entry;
pub mod moderation_list_all_debug_logs;
pub mod moderation_list_debug_logs_for_token;
pub mod moderation_list_debug_logs_for_user;
@@ -0,0 +1,76 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
use enums::error::enum_error::EnumError;
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use tokens::tokens::users::UserToken;
use crate::moderation::debug_logs::debug_log_entry::ModerationDebugLogUser;
pub const MODERATION_LIST_ALL_DEBUG_LOGS_PATH: &str = "/v1/moderation/debug_logs/list_all";
// ── Query params ──
#[derive(Deserialize, IntoParams, ToSchema)]
pub struct ModerationListAllDebugLogsQueryParams {
/// Optional comma-separated list of severity levels to include,
/// eg. `severities=error,warn`. Omit to include all rows.
pub severities: Option<String>,
/// Cursor for pagination. Pass the `next_cursor` from a previous response.
pub cursor: Option<u64>,
/// Max rows per page (default 50, max 200).
pub limit: Option<u32>,
}
impl ModerationListAllDebugLogsQueryParams {
/// Parse the comma-separated `severities` parameter.
/// Returns `None` when the parameter is absent or empty.
pub fn parsed_severities(&self) -> Result<Option<Vec<DebugLogLevel>>, EnumError> {
let raw = match self.severities.as_deref() {
Some(value) if !value.trim().is_empty() => value,
_ => return Ok(None),
};
let levels = raw
.split(',')
.map(str::trim)
.filter(|part| !part.is_empty())
.map(DebugLogLevel::from_str)
.collect::<Result<Vec<DebugLogLevel>, EnumError>>()?;
Ok(if levels.is_empty() { None } else { Some(levels) })
}
}
// ── Response ──
#[derive(Serialize, ToSchema)]
pub struct ModerationListAllDebugLogsSuccessResponse {
pub success: bool,
/// Most recent first.
pub debug_logs: Vec<ModerationListAllDebugLogsEntry>,
/// Cursor for the next page. `None` if there are no more results.
pub next_cursor: Option<u64>,
}
/// A debug log row with the creator user joined in (when one exists).
#[derive(Serialize, ToSchema)]
pub struct ModerationListAllDebugLogsEntry {
pub id: u64,
pub event_token: DebugLogEventToken,
pub debug_log_type: DebugLogType,
pub maybe_log_level: Option<DebugLogLevel>,
pub maybe_creator_user_token: Option<UserToken>,
/// The client IP address of the request (if recorded).
pub maybe_ip_address: Option<String>,
/// The request URL (if recorded; truncated to 255 chars).
pub maybe_url: Option<String>,
pub message: String,
pub created_at: DateTime<Utc>,
pub maybe_user: Option<ModerationDebugLogUser>,
}
@@ -0,0 +1,30 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use crate::moderation::debug_logs::debug_log_entry::ModerationDebugLogEntry;
pub const MODERATION_LIST_DEBUG_LOGS_FOR_TOKEN_PATH: &str = "/v1/moderation/debug_logs/list/{token}";
// ── Path params ──
#[derive(Deserialize, ToSchema)]
pub struct ModerationListDebugLogsForTokenPathInfo {
pub token: DebugLogEventToken,
}
// ── Query params ──
#[derive(Deserialize, IntoParams, ToSchema)]
pub struct ModerationListDebugLogsForTokenQueryParams {
pub limit: Option<u32>,
}
// ── Response ──
#[derive(Serialize, ToSchema)]
pub struct ModerationListDebugLogsForTokenSuccessResponse {
pub success: bool,
pub debug_logs: Vec<ModerationDebugLogEntry>,
}
@@ -0,0 +1,37 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use tokens::tokens::users::UserToken;
use crate::moderation::debug_logs::debug_log_entry::ModerationDebugLogEntry;
pub const MODERATION_LIST_DEBUG_LOGS_FOR_USER_PATH: &str = "/v1/moderation/debug_logs/user_list/{user_token}";
// ── Path params ──
#[derive(Deserialize, ToSchema)]
pub struct ModerationListDebugLogsForUserPathInfo {
pub user_token: UserToken,
}
// ── Query params ──
#[derive(Deserialize, IntoParams, ToSchema)]
pub struct ModerationListDebugLogsForUserQueryParams {
/// Cursor for pagination. Pass the `next_cursor` from a previous response.
pub cursor: Option<u64>,
/// Max rows per page (default 50, max 200).
pub limit: Option<u32>,
}
// ── Response ──
#[derive(Serialize, ToSchema)]
pub struct ModerationListDebugLogsForUserSuccessResponse {
pub success: bool,
/// Most recent first.
pub debug_logs: Vec<ModerationDebugLogEntry>,
/// Cursor for the next page. `None` if there are no more results.
pub next_cursor: Option<u64>,
}
@@ -1,4 +1,5 @@
pub mod alerts;
pub mod debug_logs;
pub mod jobs;
pub mod user;
pub mod user_referrals;
+1
View File
@@ -25,6 +25,7 @@ workspace-build-acceleration = { version = "0.1", path = "../../workspace-build-
# External
dotenv.workspace = true
env_logger = "0.11.3"
trace_id.workspace = true
log = "0.4.14"
[dev-dependencies]
+32 -1
View File
@@ -29,6 +29,37 @@ pub fn init_env_logger(default_if_absent: Option<&str>) {
env::set_var(ENV_RUST_LOG, default_log_level);
}
env_logger::init();
// Custom format so that log lines emitted while serving an HTTP request
// automatically carry the request's trace id (see the `trace_id` crate and
// the `TraceIdMiddleware` in storyteller-web). Outside of a request scope
// the trace segment is simply omitted.
env_logger::Builder::from_env(env_logger::Env::default())
.format(|buf, record| {
use std::io::Write;
let timestamp = buf.timestamp();
let level_style = buf.default_level_style(record.level());
match trace_id::current_trace_id() {
Some(trace) => writeln!(
buf,
"[{} {level_style}{}{level_style:#} {} {}] {}",
timestamp,
record.level(),
record.target(),
trace,
record.args(),
),
None => writeln!(
buf,
"[{} {level_style}{}{level_style:#} {}] {}",
timestamp,
record.level(),
record.target(),
record.args(),
),
}
})
.init();
}
+1
View File
@@ -25,6 +25,7 @@ workspace-build-acceleration = { workspace = true }
# External
env_logger = "0.11.3"
trace_id.workspace = true
log = { workspace = true }
[dev-dependencies]
+32 -1
View File
@@ -68,6 +68,37 @@ pub fn init_env_logger(default_if_absent: Option<&str>) {
env::set_var(ENV_RUST_LOG, default_log_level);
}
env_logger::init();
// Custom format so that log lines emitted while serving an HTTP request
// automatically carry the request's trace id (see the `trace_id` crate and
// the `TraceIdMiddleware` in storyteller-web). Outside of a request scope
// the trace segment is simply omitted.
env_logger::Builder::from_env(env_logger::Env::default())
.format(|buf, record| {
use std::io::Write;
let timestamp = buf.timestamp();
let level_style = buf.default_level_style(record.level());
match trace_id::current_trace_id() {
Some(trace) => writeln!(
buf,
"[{} {level_style}{}{level_style:#} {} {}] {}",
timestamp,
record.level(),
record.target(),
trace,
record.args(),
),
None => writeln!(
buf,
"[{} {level_style}{}{level_style:#} {}] {}",
timestamp,
record.level(),
record.target(),
record.args(),
),
}
})
.init();
}
+1
View File
@@ -24,6 +24,7 @@ chrono.workspace = true
sha2.workspace = true
log.workspace = true
tokio.workspace = true
trace_id.workspace = true
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
@@ -49,6 +49,9 @@ pub struct NotificationDetails {
/// Third-party identifier associated with the event, if any.
pub(crate) third_party_id: Option<String>,
/// The request-scoped trace id associated with the event, if any.
pub(crate) trace_id: Option<String>,
/// IP address of the request that triggered this notification, if any.
pub(crate) request_ip_address: Option<String>,
@@ -189,6 +192,10 @@ impl NotificationDetails {
parts.push(format!("Request IP: {}", ip));
}
if let Some(trace_id) = &self.trace_id {
parts.push(format!("Trace ID: {}", trace_id));
}
if parts.is_empty() { None } else { Some(parts.join("\n")) }
}
@@ -31,6 +31,10 @@ pub struct NotificationDetailsBuilder {
pub(crate) avt_cookie_token: Option<String>,
pub(crate) session_token: Option<String>,
pub(crate) session_user_token: Option<String>,
/// The request-scoped trace id, if any. Auto-filled from the tokio
/// task-local at `build()` time when not explicitly set.
pub(crate) trace_id: Option<String>,
}
impl NotificationDetailsBuilder {
@@ -57,6 +61,7 @@ impl NotificationDetailsBuilder {
avt_cookie_token: None,
session_token: None,
session_user_token: None,
trace_id: None,
}
}
@@ -87,6 +92,7 @@ impl NotificationDetailsBuilder {
avt_cookie_token: None,
session_token: None,
session_user_token: None,
trace_id: None,
}
}
@@ -167,6 +173,11 @@ impl NotificationDetailsBuilder {
self
}
pub fn set_trace_id(mut self, trace_id: Option<String>) -> Self {
self.trace_id = trace_id;
self
}
pub fn set_session_user_token(mut self, session_user_token: Option<String>) -> Self {
self.session_user_token = session_user_token;
self
@@ -199,6 +210,11 @@ impl NotificationDetailsBuilder {
avt_cookie_token: self.avt_cookie_token,
session_token: self.session_token,
session_user_token: self.session_user_token,
// Auto-fill from the request's task-local so any page fired while
// serving a request is correlated even if the caller never set it.
trace_id: self.trace_id.or_else(|| {
trace_id::current_trace_id().map(|t| t.to_string())
}),
}
}
@@ -3,6 +3,7 @@ use std::marker::PhantomData;
use log::warn;
use sqlx::{Executor, MySql, QueryBuilder};
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
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;
@@ -18,6 +19,12 @@ where
/// The user associated with these debug log events (if any).
pub maybe_creator_user_token: Option<&'e UserToken>,
/// The client IP address of the request (if any). Truncated on insert.
pub maybe_ip_address: Option<&'e str>,
/// The request URL (if any). Truncated to 255 characters on insert.
pub maybe_url: Option<&'e str>,
/// The events to insert.
pub events: Vec<DebugLogEvent>,
@@ -31,6 +38,7 @@ where
/// Callers should use `serde_json::to_string(...)` before constructing this.
pub struct DebugLogEvent {
pub debug_log_type: DebugLogType,
pub maybe_log_level: Option<DebugLogLevel>,
pub message: String,
}
@@ -54,9 +62,11 @@ where
}
let maybe_user_token_str = args.maybe_creator_user_token.map(|t| t.as_str().to_string());
let maybe_ip_address = args.maybe_ip_address.map(|ip| truncate_chars(ip, MAX_IP_ADDRESS_CHARS));
let maybe_url = args.maybe_url.map(|url| truncate_chars(url, MAX_URL_CHARS));
let mut query_builder = QueryBuilder::new(
"INSERT INTO debug_logs (event_token, debug_log_type, maybe_creator_user_token, message) VALUES "
"INSERT INTO debug_logs (event_token, debug_log_type, maybe_log_level, maybe_creator_user_token, maybe_ip_address, maybe_url, message) VALUES "
);
for (i, event) in args.events.iter().enumerate() {
@@ -65,8 +75,14 @@ where
query_builder.push(", ");
query_builder.push_bind(event.debug_log_type.to_str());
query_builder.push(", ");
query_builder.push_bind(event.maybe_log_level.map(|l| l.to_str()));
query_builder.push(", ");
query_builder.push_bind(&maybe_user_token_str);
query_builder.push(", ");
query_builder.push_bind(maybe_ip_address);
query_builder.push(", ");
query_builder.push_bind(maybe_url);
query_builder.push(", ");
query_builder.push_bind(&event.message);
query_builder.push(")");
@@ -81,3 +97,17 @@ where
Ok(event_token)
}
/// Maximum stored length for `maybe_ip_address` (VARCHAR(40)).
const MAX_IP_ADDRESS_CHARS: usize = 40;
/// Maximum stored length for `maybe_url` (VARCHAR(255)).
const MAX_URL_CHARS: usize = 255;
/// Truncate to at most `max_chars` characters (UTF-8 safe).
fn truncate_chars(value: &str, max_chars: usize) -> &str {
match value.char_indices().nth(max_chars) {
Some((idx, _)) => &value[..idx],
None => value,
}
}
@@ -2,6 +2,7 @@ use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
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;
@@ -20,9 +21,18 @@ where
/// The type of debug log event.
pub debug_log_type: DebugLogType,
/// The severity level of the log event (if any).
pub maybe_log_level: Option<DebugLogLevel>,
/// The log message body (pre-serialized JSON or any text).
pub message: &'e str,
/// The client IP address of the request (if any). Truncated on insert.
pub maybe_ip_address: Option<&'e str>,
/// The request URL (if any). Truncated to 255 characters on insert.
pub maybe_url: Option<&'e str>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
@@ -47,12 +57,18 @@ INSERT INTO debug_logs
SET
event_token = ?,
debug_log_type = ?,
maybe_log_level = ?,
maybe_creator_user_token = ?,
maybe_ip_address = ?,
maybe_url = ?,
message = ?
"#,
event_token.as_str(),
args.debug_log_type.to_str(),
args.maybe_log_level.map(|l| l.to_str()),
args.maybe_creator_user_token.map(|t| t.as_str()),
args.maybe_ip_address.map(|ip| truncate_chars(ip, MAX_IP_ADDRESS_CHARS)),
args.maybe_url.map(|url| truncate_chars(url, MAX_URL_CHARS)),
args.message,
)
.execute(args.mysql_executor)
@@ -60,3 +76,17 @@ SET
Ok(event_token)
}
/// Maximum stored length for `maybe_ip_address` (VARCHAR(40)).
const MAX_IP_ADDRESS_CHARS: usize = 40;
/// Maximum stored length for `maybe_url` (VARCHAR(255)).
const MAX_URL_CHARS: usize = 255;
/// Truncate to at most `max_chars` characters (UTF-8 safe).
fn truncate_chars(value: &str, max_chars: usize) -> &str {
match value.char_indices().nth(max_chars) {
Some((idx, _)) => &value[..idx],
None => value,
}
}
@@ -0,0 +1,148 @@
use std::marker::PhantomData;
use chrono::{DateTime, Utc};
use sqlx::mysql::MySqlRow;
use sqlx::{Executor, MySql, QueryBuilder, Row};
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
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;
const MAX_LIMIT: u32 = 200;
pub struct ListAllDebugLogsArgs<'c, E>
where
E: Executor<'c, Database = MySql>,
{
/// If provided, only rows with one of these log levels are returned.
/// (Rows with a NULL log level are excluded when filtering.)
pub maybe_log_levels: Option<Vec<DebugLogLevel>>,
/// Cursor for pagination: only rows with `id` strictly below this are
/// returned. Pass the `next_cursor` from a previous page.
pub maybe_id_cursor: Option<u64>,
pub limit: Option<u32>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
pub struct ListAllDebugLogsResult {
pub debug_logs: Vec<AllDebugLogRow>,
/// Cursor for the next page. `None` if there are no more results.
pub next_cursor: Option<u64>,
}
/// A debug log row left-outer-joined against the creator user (if any).
#[derive(Debug)]
pub struct AllDebugLogRow {
pub id: u64,
pub event_token: DebugLogEventToken,
pub debug_log_type: DebugLogType,
pub maybe_log_level: Option<DebugLogLevel>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_ip_address: Option<String>,
pub maybe_url: Option<String>,
pub message: String,
pub created_at: DateTime<Utc>,
// Joined user fields (present when the creator user exists).
pub maybe_user_display_name: Option<String>,
pub maybe_user_username: Option<String>,
pub maybe_user_gravatar_hash: Option<String>,
}
/// List all debug logs, most recent first, cursor-paginated by `id`,
/// optionally scoped to a set of log levels.
///
/// NB: Uses `QueryBuilder` (not the compile-time macros) because of the
/// dynamic `WHERE maybe_log_level IN (...)` clause.
pub async fn list_all_debug_logs<'c, E>(
args: ListAllDebugLogsArgs<'c, E>,
) -> Result<ListAllDebugLogsResult, sqlx::Error>
where
E: Executor<'c, Database = MySql>,
{
let limit = args.limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
// Fetch limit + 1 so we can detect whether there's a next page.
let fetch_limit = (limit + 1) as i64;
let id_cursor = args.maybe_id_cursor.unwrap_or(u64::MAX);
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new(
r#"
SELECT
d.id,
d.event_token,
d.debug_log_type,
d.maybe_log_level,
d.maybe_creator_user_token,
d.maybe_ip_address,
d.maybe_url,
d.message,
d.created_at,
u.display_name as maybe_user_display_name,
u.username as maybe_user_username,
u.email_gravatar_hash as maybe_user_gravatar_hash
FROM debug_logs d
LEFT OUTER JOIN users u
ON u.token = d.maybe_creator_user_token
WHERE d.id < "#,
);
query_builder.push_bind(id_cursor);
if let Some(levels) = args.maybe_log_levels.as_ref().filter(|l| !l.is_empty()) {
query_builder.push(" AND d.maybe_log_level IN (");
let mut separated = query_builder.separated(", ");
for level in levels {
separated.push_bind(level.to_str());
}
separated.push_unseparated(")");
}
query_builder.push(" ORDER BY d.id DESC LIMIT ");
query_builder.push_bind(fetch_limit);
let raw_rows: Vec<MySqlRow> = query_builder
.build()
.fetch_all(args.mysql_executor)
.await?;
let mut debug_logs = raw_rows
.into_iter()
.map(row_from_mysql_row)
.collect::<Result<Vec<AllDebugLogRow>, sqlx::Error>>()?;
let next_cursor = if debug_logs.len() > limit as usize {
debug_logs.truncate(limit as usize);
debug_logs.last().map(|row| row.id)
} else {
None
};
Ok(ListAllDebugLogsResult {
debug_logs,
next_cursor,
})
}
fn row_from_mysql_row(row: MySqlRow) -> Result<AllDebugLogRow, sqlx::Error> {
let created_at: DateTime<Utc> = row.try_get("created_at")?;
Ok(AllDebugLogRow {
id: row.try_get("id")?,
event_token: row.try_get("event_token")?,
debug_log_type: row.try_get("debug_log_type")?,
maybe_log_level: row.try_get("maybe_log_level")?,
maybe_creator_user_token: row.try_get("maybe_creator_user_token")?,
maybe_ip_address: row.try_get("maybe_ip_address")?,
maybe_url: row.try_get("maybe_url")?,
message: row.try_get("message")?,
created_at,
maybe_user_display_name: row.try_get("maybe_user_display_name")?,
maybe_user_username: row.try_get("maybe_user_username")?,
maybe_user_gravatar_hash: row.try_get("maybe_user_gravatar_hash")?,
})
}
@@ -3,6 +3,7 @@ use std::marker::PhantomData;
use chrono::{DateTime, NaiveDateTime, Utc};
use sqlx::{Executor, MySql};
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
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;
@@ -24,9 +25,17 @@ pub struct DebugLogRow {
pub id: u64,
pub event_token: DebugLogEventToken,
pub debug_log_type: DebugLogType,
pub maybe_log_level: Option<DebugLogLevel>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_ip_address: Option<String>,
pub maybe_url: Option<String>,
pub message: String,
pub created_at: DateTime<Utc>,
// Joined user fields (present when the creator user exists).
pub maybe_user_display_name: Option<String>,
pub maybe_user_username: Option<String>,
pub maybe_user_gravatar_hash: Option<String>,
}
#[derive(Debug)]
@@ -34,9 +43,15 @@ struct RawDebugLogRow {
id: u64,
event_token: DebugLogEventToken,
debug_log_type: DebugLogType,
maybe_log_level: Option<DebugLogLevel>,
maybe_creator_user_token: Option<UserToken>,
maybe_ip_address: Option<String>,
maybe_url: Option<String>,
message: String,
created_at: NaiveDateTime,
maybe_user_display_name: Option<String>,
maybe_user_username: Option<String>,
maybe_user_gravatar_hash: Option<String>,
}
pub async fn list_debug_logs_for_token<'e, 'c: 'e, E>(
@@ -51,15 +66,23 @@ where
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
d.id as `id: u64`,
d.event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,
d.debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,
d.maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`,
d.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
d.maybe_ip_address,
d.maybe_url,
d.message,
d.created_at,
u.display_name as `maybe_user_display_name?`,
u.username as `maybe_user_username?`,
u.email_gravatar_hash as `maybe_user_gravatar_hash?`
FROM debug_logs d
LEFT OUTER JOIN users u
ON u.token = d.maybe_creator_user_token
WHERE d.event_token = ?
ORDER BY d.id ASC
LIMIT ?
"#,
args.event_token.as_str(),
@@ -73,9 +96,15 @@ LIMIT ?
id: row.id,
event_token: row.event_token,
debug_log_type: row.debug_log_type,
maybe_log_level: row.maybe_log_level,
maybe_creator_user_token: row.maybe_creator_user_token,
maybe_ip_address: row.maybe_ip_address,
maybe_url: row.maybe_url,
message: row.message,
created_at: row.created_at.and_utc(),
maybe_user_display_name: row.maybe_user_display_name,
maybe_user_username: row.maybe_user_username,
maybe_user_gravatar_hash: row.maybe_user_gravatar_hash,
}
}).collect();
@@ -0,0 +1,142 @@
use std::marker::PhantomData;
use chrono::{DateTime, NaiveDateTime, Utc};
use sqlx::{Executor, MySql};
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
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;
const MAX_LIMIT: u32 = 200;
pub struct ListDebugLogsForUserArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub user_token: &'e UserToken,
/// Cursor for pagination: only rows with `id` strictly below this are
/// returned. Pass the `next_cursor` from a previous page.
pub maybe_id_cursor: Option<u64>,
pub limit: Option<u32>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
pub struct ListDebugLogsForUserResult {
pub debug_logs: Vec<UserDebugLogRow>,
/// Cursor for the next page. `None` if there are no more results.
pub next_cursor: Option<u64>,
}
#[derive(Debug)]
pub struct UserDebugLogRow {
pub id: u64,
pub event_token: DebugLogEventToken,
pub debug_log_type: DebugLogType,
pub maybe_log_level: Option<DebugLogLevel>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_ip_address: Option<String>,
pub maybe_url: Option<String>,
pub message: String,
pub created_at: DateTime<Utc>,
// Joined user fields (present when the creator user exists).
pub maybe_user_display_name: Option<String>,
pub maybe_user_username: Option<String>,
pub maybe_user_gravatar_hash: Option<String>,
}
#[derive(Debug)]
struct RawUserDebugLogRow {
id: u64,
event_token: DebugLogEventToken,
debug_log_type: DebugLogType,
maybe_log_level: Option<DebugLogLevel>,
maybe_creator_user_token: Option<UserToken>,
maybe_ip_address: Option<String>,
maybe_url: Option<String>,
message: String,
created_at: NaiveDateTime,
maybe_user_display_name: Option<String>,
maybe_user_username: Option<String>,
maybe_user_gravatar_hash: Option<String>,
}
/// List debug logs for a user, most recent first, cursor-paginated by `id`.
pub async fn list_debug_logs_for_user<'e, 'c: 'e, E>(
args: ListDebugLogsForUserArgs<'e, 'c, E>,
) -> Result<ListDebugLogsForUserResult, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let limit = args.limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
// Fetch limit + 1 so we can detect whether there's a next page.
let fetch_limit = (limit + 1) as i64;
// Use u64::MAX when no cursor so all rows are included.
let id_cursor = args.maybe_id_cursor.unwrap_or(u64::MAX);
let rows = sqlx::query_as!(
RawUserDebugLogRow,
r#"
SELECT
d.id as `id: u64`,
d.event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,
d.debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,
d.maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`,
d.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
d.maybe_ip_address,
d.maybe_url,
d.message,
d.created_at,
u.display_name as `maybe_user_display_name?`,
u.username as `maybe_user_username?`,
u.email_gravatar_hash as `maybe_user_gravatar_hash?`
FROM debug_logs d
LEFT OUTER JOIN users u
ON u.token = d.maybe_creator_user_token
WHERE d.maybe_creator_user_token = ?
AND d.id < ?
ORDER BY d.id DESC
LIMIT ?
"#,
args.user_token.as_str(),
id_cursor,
fetch_limit,
)
.fetch_all(args.mysql_executor)
.await?;
let mut debug_logs: Vec<UserDebugLogRow> = rows.into_iter().map(|row| {
UserDebugLogRow {
id: row.id,
event_token: row.event_token,
debug_log_type: row.debug_log_type,
maybe_log_level: row.maybe_log_level,
maybe_creator_user_token: row.maybe_creator_user_token,
maybe_ip_address: row.maybe_ip_address,
maybe_url: row.maybe_url,
message: row.message,
created_at: row.created_at.and_utc(),
maybe_user_display_name: row.maybe_user_display_name,
maybe_user_username: row.maybe_user_username,
maybe_user_gravatar_hash: row.maybe_user_gravatar_hash,
}
}).collect();
let next_cursor = if debug_logs.len() > limit as usize {
debug_logs.truncate(limit as usize);
debug_logs.last().map(|row| row.id)
} else {
None
};
Ok(ListDebugLogsForUserResult {
debug_logs,
next_cursor,
})
}
@@ -1,3 +1,5 @@
pub mod bulk_insert_debug_logs;
pub mod insert_debug_log;
pub mod list_all_debug_logs;
pub mod list_debug_logs_for_token;
pub mod list_debug_logs_for_user;
@@ -47,7 +47,6 @@ pub mod user_spend_events;
pub mod user_spend_summaries;
pub mod users;
pub mod vocoder;
pub mod voice_clone_requests;
pub mod voice_conversion;
pub mod voice_designer;
pub mod wallet_ledger_entries;
@@ -1,53 +0,0 @@
use chrono::NaiveDateTime;
use sqlx::MySqlPool;
use errors::AnyhowResult;
use crate::helpers::numeric_converters::try_i64_to_u64_or_min;
#[derive(Clone)]
pub struct TtsQueueLengthResult {
pub record_count: u64,
pub present_time: NaiveDateTime,
}
struct QueryResultInternal {
record_count: i64,
present_time: NaiveDateTime,
}
pub async fn get_pending_tts_inference_job_count(
pool: &MySqlPool,
) -> AnyhowResult<TtsQueueLengthResult> {
// NB (1): We query as a union since "started" jobs can get picked off mid-run and go stale
// forever. We currently have no automated means of collecting those jobs.
// NB (2): We query the server timestamp so we can cache the results.
// The frontend can then monotonically adjust the count based on timestamp by ignoring
// past times.
let result : QueryResultInternal = sqlx::query_as!(
QueryResultInternal,
r#"
SELECT
COUNT(distinct token) as record_count,
NOW() as present_time
FROM
(
SELECT token
FROM tts_inference_jobs
WHERE status = "started"
AND created_at > (CURDATE() - INTERVAL 5 MINUTE)
UNION
SELECT token
FROM tts_inference_jobs
WHERE status IN ("pending", "attempt_failed")
) as t
"#,
)
.fetch_one(pool)
.await?;
Ok(TtsQueueLengthResult {
record_count: try_i64_to_u64_or_min(result.record_count),
present_time: result.present_time,
})
}
@@ -1,5 +1,4 @@
pub mod _keys;
pub mod get_pending_tts_inference_job_count;
pub mod get_pending_tts_inference_job_detailed_stats;
pub mod get_tts_inference_job_status;
pub mod insert_tts_inference_job;
@@ -1,237 +0,0 @@
use anyhow::anyhow;
use chrono::Utc;
use sqlx::MySqlPool;
use errors::AnyhowResult;
use crate::helpers::boolean_converters::i8_to_bool;
/// Lookup by voice clone request token
pub async fn get_voice_clone_request_by_token(
voice_clone_request_token: &str,
pool: &MySqlPool,
) -> AnyhowResult<Option<VoiceCloneRequest>> {
internal_voice_clone_request_by_token(voice_clone_request_token, pool).await
}
/// Lookup by user
pub async fn get_voice_clone_request_by_user_token(
user_token: &str,
pool: &MySqlPool,
) -> AnyhowResult<Option<VoiceCloneRequest>> {
internal_voice_clone_request_by_user_token(user_token, pool).await
}
/// Used for both *BY TOKEN* and for *BY USER* lookups.
/// The user field should be optional for one and not for the other, but code reuse is more
/// important and this is a quick / hasty feature.
#[derive(Debug)]
pub struct VoiceCloneRequest {
pub token: String,
// Contact
pub maybe_user_token: Option<String>,
pub email_address: String,
pub discord_username: String,
// Visibility
pub is_for_private_use: bool,
pub is_for_public_use: bool,
// Use
pub is_for_studio: bool,
pub is_for_twitch_tts: bool,
pub is_for_api_use: bool,
pub is_for_music: bool,
pub is_for_games: bool,
pub is_for_other: bool,
pub optional_notes_on_use: Option<String>,
// Subject/Ownership
pub is_own_voice: bool,
pub is_third_party_voice: bool,
pub optional_notes_on_subject: Option<String>,
// Equipment
pub has_clean_audio_recordings: bool,
pub has_good_microphone: bool,
// Comments
pub optional_questions: Option<String>,
pub optional_extra_comments: Option<String>,
pub created_at: chrono::DateTime<Utc>,
pub updated_at: chrono::DateTime<Utc>,
}
async fn internal_voice_clone_request_by_token(
voice_clone_request_token: &str,
pool: &MySqlPool,
) -> AnyhowResult<Option<VoiceCloneRequest>> {
let maybe_record = sqlx::query_as!(
VoiceCloneRequestInternal,
r#"
SELECT
token,
maybe_user_token,
email_address,
discord_username,
is_for_private_use,
is_for_public_use,
is_for_studio,
is_for_twitch_tts,
is_for_api_use,
is_for_music,
is_for_games,
is_for_other,
optional_notes_on_use,
is_own_voice,
is_third_party_voice,
optional_notes_on_subject,
has_clean_audio_recordings,
has_good_microphone,
optional_questions,
optional_extra_comments,
created_at,
updated_at
FROM voice_clone_requests
WHERE
token = ?
"#,
voice_clone_request_token,
)
.fetch_one(pool)
.await;
handle_result(maybe_record)
}
async fn internal_voice_clone_request_by_user_token(
user_token: &str,
pool: &MySqlPool,
) -> AnyhowResult<Option<VoiceCloneRequest>> {
let maybe_record = sqlx::query_as!(
VoiceCloneRequestInternal,
r#"
SELECT
token,
maybe_user_token,
email_address,
discord_username,
is_for_private_use,
is_for_public_use,
is_for_studio,
is_for_twitch_tts,
is_for_api_use,
is_for_music,
is_for_games,
is_for_other,
optional_notes_on_use,
is_own_voice,
is_third_party_voice,
optional_notes_on_subject,
has_clean_audio_recordings,
has_good_microphone,
optional_questions,
optional_extra_comments,
created_at,
updated_at
FROM voice_clone_requests
WHERE
maybe_user_token = ?
"#,
user_token,
)
.fetch_one(pool)
.await;
handle_result(maybe_record)
}
fn handle_result(
maybe_record: Result<VoiceCloneRequestInternal, sqlx::Error>,
) -> AnyhowResult<Option<VoiceCloneRequest>> {
let record : VoiceCloneRequestInternal = match maybe_record {
Ok(record) => record,
Err(sqlx::Error::RowNotFound) => return Ok(None),
Err(ref err) => return Err(anyhow!("database query error: {:?}", err)),
};
Ok(Some(VoiceCloneRequest {
token: record.token,
maybe_user_token: record.maybe_user_token,
email_address: record.email_address,
discord_username: record.discord_username,
is_for_private_use: i8_to_bool(record.is_for_private_use),
is_for_public_use: i8_to_bool(record.is_for_public_use),
is_for_studio: i8_to_bool(record.is_for_studio),
is_for_twitch_tts: i8_to_bool(record.is_for_twitch_tts),
is_for_api_use: i8_to_bool(record.is_for_api_use),
is_for_music: i8_to_bool(record.is_for_music),
is_for_games: i8_to_bool(record.is_for_games),
is_for_other: i8_to_bool(record.is_for_other),
optional_notes_on_use: record.optional_notes_on_use,
is_own_voice: i8_to_bool(record.is_own_voice),
is_third_party_voice: i8_to_bool(record.is_third_party_voice),
optional_notes_on_subject: record.optional_notes_on_subject,
has_clean_audio_recordings: i8_to_bool(record.has_clean_audio_recordings),
has_good_microphone: i8_to_bool(record.has_good_microphone),
optional_questions: record.optional_questions,
optional_extra_comments: record.optional_extra_comments,
created_at: record.created_at,
updated_at: record.updated_at,
}))
}
#[derive(Debug)]
struct VoiceCloneRequestInternal {
pub token: String,
// Contact
pub maybe_user_token: Option<String>,
pub email_address: String,
pub discord_username: String,
// Visibility
pub is_for_private_use: i8,
pub is_for_public_use: i8,
// Use
pub is_for_studio: i8,
pub is_for_twitch_tts: i8,
pub is_for_api_use: i8,
pub is_for_music: i8,
pub is_for_games: i8,
pub is_for_other: i8,
pub optional_notes_on_use: Option<String>,
// Subject/Ownership
pub is_own_voice: i8,
pub is_third_party_voice: i8,
pub optional_notes_on_subject: Option<String>,
// Equipment
pub has_clean_audio_recordings: i8,
pub has_good_microphone: i8,
// Comments
pub optional_questions: Option<String>,
pub optional_extra_comments: Option<String>,
pub created_at: chrono::DateTime<Utc>,
pub updated_at: chrono::DateTime<Utc>,
}
@@ -1,125 +0,0 @@
use anyhow::anyhow;
use sqlx::MySqlPool;
use errors::AnyhowResult;
use tokens::tokens::voice_clone_requests::VoiceCloneRequestToken;
pub struct InsertVoiceCloneRequestBuilder {
pub uuid_idempotency_token: String,
// Contact
pub maybe_user_token: Option<String>,
pub email_address: String,
pub discord_username: String,
// Visibility
pub is_for_private_use: bool,
pub is_for_public_use: bool,
// Use
pub is_for_studio: bool,
pub is_for_twitch_tts: bool,
pub is_for_api_use: bool,
pub is_for_music: bool,
pub is_for_games: bool,
pub is_for_other: bool,
pub optional_notes_on_use: Option<String>,
// Subject/Ownership
pub is_own_voice: bool,
pub is_third_party_voice: bool,
pub optional_notes_on_subject: Option<String>,
// Equipment
pub has_clean_audio_recordings: bool,
pub has_good_microphone: bool,
// Comments
pub optional_questions: Option<String>,
pub optional_extra_comments: Option<String>,
pub ip_address_creation: String,
}
impl InsertVoiceCloneRequestBuilder {
/// Returns the newly generated token.
pub async fn insert(&self, mysql_pool: &MySqlPool) -> AnyhowResult<String> {
let token = VoiceCloneRequestToken::generate().to_string();
let query = sqlx::query!(
r#"
INSERT INTO voice_clone_requests
SET
token = ?,
uuid_idempotency_token = ?,
maybe_user_token = ?,
email_address = ?,
discord_username = ?,
is_for_private_use = ?,
is_for_public_use = ?,
is_for_studio = ?,
is_for_twitch_tts = ?,
is_for_api_use = ?,
is_for_music = ?,
is_for_games = ?,
is_for_other = ?,
optional_notes_on_use = ?,
is_own_voice = ?,
is_third_party_voice = ?,
optional_notes_on_subject = ?,
has_clean_audio_recordings = ?,
has_good_microphone = ?,
optional_questions = ?,
optional_extra_comments = ?,
ip_address_creation = ?
"#,
&token,
&self.uuid_idempotency_token,
&self.maybe_user_token,
&self.email_address,
&self.discord_username,
&self.is_for_private_use,
&self.is_for_public_use,
&self.is_for_studio,
&self.is_for_twitch_tts,
&self.is_for_api_use,
&self.is_for_music,
&self.is_for_games,
&self.is_for_other,
&self.optional_notes_on_use,
&self.is_own_voice,
&self.is_third_party_voice,
&self.optional_notes_on_subject,
&self.has_clean_audio_recordings,
&self.has_good_microphone,
&self.optional_questions,
&self.optional_extra_comments,
&self.ip_address_creation,
);
let query_result = query.execute(mysql_pool).await;
let _record_id = match query_result {
Ok(res) => res.last_insert_id(),
Err(err) => return Err(anyhow!("Error creating Voice Clone Request: {:?}", err)),
};
Ok(token)
}
}
@@ -1,2 +0,0 @@
pub mod get_voice_clone_request;
pub mod insert_voice_clone_requests_builder;
@@ -0,0 +1,151 @@
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 = 16;
/// Used in the `debug_logs` table in a `VARCHAR(16)` field (`maybe_log_level`).
///
/// 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 DebugLogLevel {
Info,
Warn,
Error,
Debug,
Trace,
}
impl_enum_display_and_debug_using_to_str!(DebugLogLevel);
impl_mysql_enum_coders!(DebugLogLevel);
impl_mysql_from_row!(DebugLogLevel);
impl DebugLogLevel {
pub fn to_str(&self) -> &'static str {
match self {
Self::Info => "info",
Self::Warn => "warn",
Self::Error => "error",
Self::Debug => "debug",
Self::Trace => "trace",
}
}
pub fn from_str(value: &str) -> Result<Self, EnumError> {
match value {
"info" => Ok(Self::Info),
"warn" => Ok(Self::Warn),
"error" => Ok(Self::Error),
"debug" => Ok(Self::Debug),
"trace" => Ok(Self::Trace),
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
}
}
pub fn all_variants() -> BTreeSet<Self> {
BTreeSet::from([
Self::Info,
Self::Warn,
Self::Error,
Self::Debug,
Self::Trace,
])
}
}
#[cfg(test)]
mod tests {
use crate::by_table::debug_logs::debug_log_level::DebugLogLevel;
use crate::by_table::debug_logs::debug_log_level::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(DebugLogLevel::Info, "info");
assert_serialization(DebugLogLevel::Warn, "warn");
assert_serialization(DebugLogLevel::Error, "error");
assert_serialization(DebugLogLevel::Debug, "debug");
assert_serialization(DebugLogLevel::Trace, "trace");
}
#[test]
fn to_str() {
assert_eq!(DebugLogLevel::Info.to_str(), "info");
assert_eq!(DebugLogLevel::Warn.to_str(), "warn");
assert_eq!(DebugLogLevel::Error.to_str(), "error");
assert_eq!(DebugLogLevel::Debug.to_str(), "debug");
assert_eq!(DebugLogLevel::Trace.to_str(), "trace");
}
#[test]
fn from_str() {
assert_eq!(DebugLogLevel::from_str("info").unwrap(), DebugLogLevel::Info);
assert_eq!(DebugLogLevel::from_str("warn").unwrap(), DebugLogLevel::Warn);
assert_eq!(DebugLogLevel::from_str("error").unwrap(), DebugLogLevel::Error);
assert_eq!(DebugLogLevel::from_str("debug").unwrap(), DebugLogLevel::Debug);
assert_eq!(DebugLogLevel::from_str("trace").unwrap(), DebugLogLevel::Trace);
}
#[test]
fn from_str_err() {
let result = DebugLogLevel::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 = DebugLogLevel::all_variants();
assert_eq!(variants.len(), 5);
assert_eq!(variants.pop_first(), Some(DebugLogLevel::Info));
assert_eq!(variants.pop_first(), Some(DebugLogLevel::Warn));
assert_eq!(variants.pop_first(), Some(DebugLogLevel::Error));
assert_eq!(variants.pop_first(), Some(DebugLogLevel::Debug));
assert_eq!(variants.pop_first(), Some(DebugLogLevel::Trace));
assert_eq!(variants.pop_first(), None);
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(DebugLogLevel::all_variants().len(), DebugLogLevel::iter().len());
}
#[test]
fn round_trip() {
for variant in DebugLogLevel::all_variants() {
assert_eq!(variant, DebugLogLevel::from_str(variant.to_str()).unwrap());
assert_eq!(variant, DebugLogLevel::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, DebugLogLevel::from_str(&format!("{:?}", variant)).unwrap());
}
}
#[test]
fn max_length() {
for variant in DebugLogLevel::all_variants() {
assert!(variant.to_str().len() <= MAX_LENGTH);
}
}
}
}
@@ -18,7 +18,16 @@ pub const MAX_LENGTH: usize = 24;
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum DebugLogType {
// ========== First party ==========
/// A user request, typically the Json<T> actix_web handlers read.
HttpRequest,
/// A backend/pipeline failure while serving a request.
BackendFailure,
// ========== Third party ==========
FalRequest,
KinoviRequest,
GrokApiRequest,
@@ -34,6 +43,7 @@ impl DebugLogType {
pub fn to_str(&self) -> &'static str {
match self {
Self::HttpRequest => "http_request",
Self::BackendFailure => "backend_failure",
Self::FalRequest => "fal_request",
Self::KinoviRequest => "kinovi_request",
Self::GrokApiRequest => "grok_api_request",
@@ -45,6 +55,7 @@ impl DebugLogType {
pub fn from_str(value: &str) -> Result<Self, EnumError> {
match value {
"http_request" => Ok(Self::HttpRequest),
"backend_failure" => Ok(Self::BackendFailure),
"fal_request" => Ok(Self::FalRequest),
"kinovi_request" => Ok(Self::KinoviRequest),
"grok_api_request" => Ok(Self::GrokApiRequest),
@@ -57,6 +68,7 @@ impl DebugLogType {
pub fn all_variants() -> BTreeSet<Self> {
BTreeSet::from([
Self::HttpRequest,
Self::BackendFailure,
Self::FalRequest,
Self::KinoviRequest,
Self::GrokApiRequest,
@@ -79,6 +91,7 @@ mod tests {
#[test]
fn test_serialization() {
assert_serialization(DebugLogType::HttpRequest, "http_request");
assert_serialization(DebugLogType::BackendFailure, "backend_failure");
assert_serialization(DebugLogType::FalRequest, "fal_request");
assert_serialization(DebugLogType::KinoviRequest, "kinovi_request");
assert_serialization(DebugLogType::GrokApiRequest, "grok_api_request");
@@ -89,6 +102,7 @@ mod tests {
#[test]
fn to_str() {
assert_eq!(DebugLogType::HttpRequest.to_str(), "http_request");
assert_eq!(DebugLogType::BackendFailure.to_str(), "backend_failure");
assert_eq!(DebugLogType::FalRequest.to_str(), "fal_request");
assert_eq!(DebugLogType::KinoviRequest.to_str(), "kinovi_request");
assert_eq!(DebugLogType::GrokApiRequest.to_str(), "grok_api_request");
@@ -99,6 +113,7 @@ mod tests {
#[test]
fn from_str() {
assert_eq!(DebugLogType::from_str("http_request").unwrap(), DebugLogType::HttpRequest);
assert_eq!(DebugLogType::from_str("backend_failure").unwrap(), DebugLogType::BackendFailure);
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("grok_api_request").unwrap(), DebugLogType::GrokApiRequest);
@@ -120,8 +135,9 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = DebugLogType::all_variants();
assert_eq!(variants.len(), 6);
assert_eq!(variants.len(), 7);
assert_eq!(variants.pop_first(), Some(DebugLogType::HttpRequest));
assert_eq!(variants.pop_first(), Some(DebugLogType::BackendFailure));
assert_eq!(variants.pop_first(), Some(DebugLogType::FalRequest));
assert_eq!(variants.pop_first(), Some(DebugLogType::KinoviRequest));
assert_eq!(variants.pop_first(), Some(DebugLogType::GrokApiRequest));
@@ -1 +1,2 @@
pub mod debug_log_level;
pub mod debug_log_type;
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "trace_id"
edition = "2018"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
"Brandon Thomas <echelon@gmail.com>",
]
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "trace_id"
path = "src/lib.rs"
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration.workspace = true
rand = { version = "0.8.5", features = ["std_rng"] }
serde.workspace = true
serde_derive.workspace = true
tokio.workspace = true
+156
View File
@@ -0,0 +1,156 @@
//! trace_id
//!
//! Request-scoped trace identifiers for observability.
//!
//! A `TraceId` is generated for every inbound HTTP request (see the
//! `trace_id_middleware` in `storyteller-web`), stored in a tokio task-local,
//! and emitted with every log line and pager notification produced while
//! serving that request.
use std::fmt;
use serde_derive::{Deserialize, Serialize};
/// The string prefix for trace ids.
pub const TRACE_ID_PREFIX: &str = "trace_";
/// Number of Crockford base32 characters after the prefix.
/// 26 characters × 5 bits = 130 bits of entropy (≥ the 128-bit industry
/// standard for trace ids). Total length: 6 + 26 = 32 characters.
pub const TRACE_ID_ENTROPY_CHARS: usize = 26;
/// Total serialized length of a trace id ("trace_" + entropy).
pub const TRACE_ID_TOTAL_LENGTH: usize = 32;
/// Crockford base32 alphabet, lowercase (no i, l, o, u).
const CROCKFORD_LOWERCASE_CHARSET: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz";
/// A unique-per-request trace identifier, eg. `trace_1h3x9k2m4p7q8r5t6v0w2y3z4a`.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub struct TraceId(String);
impl TraceId {
/// Generate a new random trace id. Infallible.
#[inline]
pub fn generate() -> Self {
use rand::Rng;
let mut rng = rand::thread_rng();
let entropy: String = (0..TRACE_ID_ENTROPY_CHARS)
.map(|_| {
let idx = rng.gen_range(0..CROCKFORD_LOWERCASE_CHARSET.len());
CROCKFORD_LOWERCASE_CHARSET[idx] as char
})
.collect();
TraceId(format!("{}{}", TRACE_ID_PREFIX, entropy))
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for TraceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl fmt::Debug for TraceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
tokio::task_local! {
/// The trace id for the currently-executing request, if any.
///
/// Set by the HTTP middleware via [`TRACE_ID_TASK_LOCAL.scope(...)`], and
/// readable from anywhere within the request's task — including the
/// logger's format closure and the pager — via [`current_trace_id`].
pub static TRACE_ID_TASK_LOCAL: TraceId;
}
/// The trace id of the currently-executing request, if one is in scope.
///
/// Returns `None` outside of a request context (startup, background threads,
/// detached `tokio::spawn` tasks that weren't explicitly re-scoped).
#[inline]
pub fn current_trace_id() -> Option<TraceId> {
TRACE_ID_TASK_LOCAL.try_with(|trace_id| trace_id.clone()).ok()
}
#[cfg(test)]
mod tests {
use super::*;
mod generation {
use super::*;
#[test]
fn total_length_is_32() {
assert_eq!(TraceId::generate().as_str().len(), TRACE_ID_TOTAL_LENGTH);
}
#[test]
fn has_prefix() {
assert!(TraceId::generate().as_str().starts_with(TRACE_ID_PREFIX));
}
#[test]
fn entropy_uses_crockford_lowercase_only() {
let trace_id = TraceId::generate();
let entropy = &trace_id.as_str()[TRACE_ID_PREFIX.len()..];
assert_eq!(entropy.len(), TRACE_ID_ENTROPY_CHARS);
for c in entropy.chars() {
assert!(
CROCKFORD_LOWERCASE_CHARSET.contains(&(c as u8)),
"unexpected character: {}",
c
);
}
}
#[test]
fn ids_are_unique() {
let a = TraceId::generate();
let b = TraceId::generate();
assert_ne!(a, b);
}
}
mod task_local {
use super::*;
#[tokio::test]
async fn absent_outside_scope() {
assert!(current_trace_id().is_none());
}
#[tokio::test]
async fn present_inside_scope() {
let trace_id = TraceId::generate();
let expected = trace_id.clone();
TRACE_ID_TASK_LOCAL
.scope(trace_id, async move {
assert_eq!(current_trace_id(), Some(expected));
})
.await;
}
#[tokio::test]
async fn survives_await_points() {
let trace_id = TraceId::generate();
let expected = trace_id.clone();
TRACE_ID_TASK_LOCAL
.scope(trace_id, async move {
tokio::task::yield_now().await;
assert_eq!(current_trace_id(), Some(expected));
})
.await;
}
}
}
@@ -89,6 +89,7 @@ seedance2pro_client.workspace = true
server_environment = { path = "../../../lib/server_environment" }
thumbnail_generator = { path = "../../../api_clients/thumbnail_generator" }
tokens.workspace = true
trace_id.workspace = true
tts_common = { path = "../../../lib/deprecated/tts_common" }
url_config = { path = "../../../lib/url_config" }
url_utils = { path = "../../../lib/url_utils" }
@@ -88,7 +88,10 @@ use crate::http_server::endpoints::media_files::upsert_upload::write_error::Medi
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 artcraft_api_defs::generate::video::edit::beeble_switchx_edit_video::*;
use crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_token_handler::*;
use artcraft_api_defs::moderation::debug_logs::debug_log_entry::*;
use artcraft_api_defs::moderation::debug_logs::moderation_list_all_debug_logs::*;
use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_token::*;
use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_user::*;
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::*;
@@ -214,8 +217,6 @@ use billing_component::stripe::http_endpoints::checkout::create::stripe_create_c
use crate::http_server::endpoints::billing_fakeyou::list_active_user_subscriptions_handler::*;
use crate::http_server::endpoints::service::status_alert_handler::*;
use crate::http_server::endpoints::stats::get_unified_queue_stats_handler::*;
use crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::*;
use crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::EnqueueStudioGen2Request;
use crate::http_server::endpoints::tags::list_tags_for_entity_handler::*;
use crate::http_server::endpoints::tags::set_tags_for_entity_handler::*;
use crate::http_server::endpoints::tts::enqueue_infer_tts_handler::enqueue_infer_tts_handler::*;
@@ -238,9 +239,6 @@ use crate::http_server::endpoints::users::login_handler::LoginErrorResponse;
use crate::http_server::endpoints::users::logout_handler::*;
use crate::http_server::endpoints::users::session_info_handler::*;
use crate::http_server::endpoints::users::session_token_info_handler::*;
use crate::http_server::endpoints::voice_conversion::enqueue_voice_conversion_inference_handler::*;
use crate::http_server::endpoints::voice_designer::inference::enqueue_tts_request::*;
use crate::http_server::endpoints::voice_designer::voice_datasets::list_datasets_by_user::*;
use crate::http_server::endpoints::weights::delete::delete_weight_handler::*;
use crate::http_server::endpoints::weights::get::get_weight_handler::*;
use crate::http_server::endpoints::weights::list::list_available_weights_handler::*;
@@ -385,7 +383,6 @@ use crate::http_server::endpoints::video_info::video_info_upload_handler::*;
use crate::http_server::endpoints::video_info::video_info_notes_handler::*;
use crate::http_server::endpoints::web_referrals::log_web_referral_handler::*;
use crate::http_server::endpoints::image_studio::update_gpt_image_job_status_handler::*;
use crate::http_server::endpoints::voice_conversion::enqueue_seed_vc_inference_handler::*;
use crate::http_server::endpoints::credits::get_session_credits_handler::*;
use crate::http_server::endpoints::subscriptions::get_session_subscription_handler::*;
use crate::http_server::endpoints::analytics::log_app_active_user_handler::*;
@@ -484,7 +481,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::prompts::get_prompt_handler::get_prompt_handler,
crate::http_server::endpoints::service::status_alert_handler::status_alert_handler,
crate::http_server::endpoints::stats::get_unified_queue_stats_handler::get_unified_queue_stats_handler,
crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::enqueue_studio_gen2_handler,
crate::http_server::endpoints::tags::list_tags_for_entity_handler::list_tags_for_entity_handler,
crate::http_server::endpoints::tags::set_tags_for_entity_handler::set_tags_for_entity_handler,
crate::http_server::endpoints::tts::enqueue_infer_tts_handler::enqueue_infer_tts_handler::enqueue_infer_tts_handler,
@@ -506,9 +502,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::users::logout_handler::logout_handler,
crate::http_server::endpoints::users::session_info_handler::session_info_handler,
crate::http_server::endpoints::users::session_token_info_handler::session_token_info_handler,
crate::http_server::endpoints::voice_conversion::enqueue_voice_conversion_inference_handler::enqueue_voice_conversion_inference_handler,
crate::http_server::endpoints::voice_designer::inference::enqueue_tts_request::enqueue_tts_request,
crate::http_server::endpoints::voice_designer::voice_datasets::list_datasets_by_user::list_datasets_by_user_handler,
crate::http_server::endpoints::weights::delete::delete_weight_handler::delete_weight_handler,
crate::http_server::endpoints::weights::get::get_weight_handler::get_weight_handler,
crate::http_server::endpoints::weights::list::list_available_weights_handler::list_available_weights_handler,
@@ -564,9 +557,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::analytics::log_app_active_user_handler::log_app_active_user_handler,
crate::http_server::endpoints::analytics::log_app_active_user_json_handler::log_app_active_user_json_handler,
// Voice Conversion
crate::http_server::endpoints::voice_conversion::enqueue_seed_vc_inference_handler::enqueue_infer_seed_vc_handler,
// TTS
crate::http_server::endpoints::tts::enqueue_infer_f5_tts_handler::enqueue_infer_f5_tts_handler::enqueue_infer_f5_tts_handler,
// Characters
crate::http_server::endpoints::characters::list_characters_handler::list_characters_handler,
crate::http_server::endpoints::characters::create_character_handler::create_character_handler,
@@ -602,7 +593,9 @@ 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_all_debug_logs_handler::moderation_list_all_debug_logs_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::debug_logs::moderation_list_debug_logs_for_user_handler::moderation_list_debug_logs_for_user_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_referrals::moderator_list_global_user_referrals_handler::moderator_list_global_user_referrals_handler,
@@ -926,17 +919,10 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
EnqueueLivePortraitCropDimensions,
EnqueueLivePortraitWorkflowRequest,
EnqueueLivePortraitWorkflowSuccessResponse,
EnqueueStudioGen2Request,
EnqueueStudioGen2Response,
EnqueueTTSRequest,
EnqueueTTSRequestSuccessResponse,
EnqueueVideoStyleTransferRequest,
EnqueueVideoStyleTransferSuccessResponse,
EnqueueVoiceConversionInferenceRequest,
EnqueueVoiceConversionInferenceSuccessResponse,
FeaturedMediaFile,
FeaturedModelWeightForList,
FundamentalFrequencyMethod,
GenerateFlux1DevTextToImageAspectRatio,
GenerateFlux1DevTextToImageNumImages,
GenerateFlux1DevTextToImageRequest,
@@ -1016,9 +1002,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
ListCommentsError,
ListCommentsPathInfo,
ListCommentsSuccessResponse,
ListDatasetsByUserError,
ListDatasetsByUserPathInfo,
ListDatasetsByUserSuccessResponse,
ListFeaturedMediaFilesQueryParams,
ListFeaturedMediaFilesSuccessResponse,
ListFeaturedWeightsError,
@@ -1030,10 +1013,17 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
GetJobByTokenPathInfo,
GetJobByTokenSuccessResponse,
ModerationJobResponse,
ListDebugLogsPathInfo,
ListDebugLogsQueryParams,
ListDebugLogsSuccessResponse,
DebugLogEntry,
ModerationDebugLogEntry,
ModerationDebugLogUser,
ModerationListAllDebugLogsEntry,
ModerationListAllDebugLogsQueryParams,
ModerationListAllDebugLogsSuccessResponse,
ModerationListDebugLogsForTokenPathInfo,
ModerationListDebugLogsForTokenQueryParams,
ModerationListDebugLogsForTokenSuccessResponse,
ModerationListDebugLogsForUserPathInfo,
ModerationListDebugLogsForUserQueryParams,
ModerationListDebugLogsForUserSuccessResponse,
ListStaffAuditLogsQueryParams,
ListStaffAuditLogsSuccessResponse,
ListImpersonationRequestsQueryParams,
@@ -1182,7 +1172,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
WeightsData,
WriteEngineAssetMediaSuccessResponse,
WriteSceneFileMediaSuccessResponse,
ZsDatasetRecord,
// Cost Estimate types
EstimateImageCostRequest,
@@ -1,18 +0,0 @@
pub const DEFAULT_INVESTOR_REDIRECT : &str = "https://fakeyou.com";
pub fn redirect_is_allowed(redirect_url: &str) -> bool {
match redirect_url {
"http://jungle.horse" => true,
"http://jungle.horse/" => true,
"https://create.storyteller.io" => true,
"https://create.storyteller.io/" => true,
"https://fakeyou.com" => true,
"https://fakeyou.com/clone" => true,
"https://jungle.horse" => true,
"https://jungle.horse/" => true,
"https://storyteller.io" => true,
"https://storyteller.io/" => true,
_ => false,
}
}
@@ -1,10 +0,0 @@
//! A temporary cookie for investor demo
use actix_web::HttpRequest;
pub const STORYTELLER_DEMO_COOKIE_NAME : &str = "storyteller_demo";
pub fn request_has_demo_cookie(http_request: &HttpRequest) -> bool {
// NB: Payload of the cookie does not matter.
http_request.cookie(STORYTELLER_DEMO_COOKIE_NAME).is_some()
}
@@ -1,70 +0,0 @@
use std::sync::Arc;
use actix_http::header;
use actix_web::cookie::time::OffsetDateTime;
use actix_web::cookie::Cookie;
use actix_web::http::StatusCode;
use actix_web::web::Query;
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use actix_helpers::extractors::get_request_host::get_request_host;
use crate::http_server::deprecated_endpoints::investor_demo::default_redirect::{redirect_is_allowed, DEFAULT_INVESTOR_REDIRECT};
use crate::http_server::deprecated_endpoints::investor_demo::demo_cookie::STORYTELLER_DEMO_COOKIE_NAME;
use crate::state::server_state::ServerState;
#[derive(Deserialize)]
pub struct QueryFields {
redirect_to: Option<String>,
}
pub async fn disable_demo_mode_handler(
http_request: HttpRequest,
query: Query<QueryFields>,
server_state: web::Data<Arc<ServerState>>
) -> impl Responder
{
let unsafe_redirect = query.redirect_to
.as_deref()
.map(|r| r.to_string())
.unwrap_or(DEFAULT_INVESTOR_REDIRECT.to_string());
let redirect_allowed = redirect_is_allowed(&unsafe_redirect);
let safe_redirect_url = if redirect_allowed {
unsafe_redirect
} else {
DEFAULT_INVESTOR_REDIRECT.to_string()
};
let maybe_host = get_request_host(&http_request);
let maybe_cookie_domain = match maybe_host {
Some("jungle.horse") | Some("api.jungle.horse") => Some(".jungle.horse"),
Some("fakeyou.com") | Some("api.fakeyou.com") => Some(".fakeyou.com"),
Some("storyteller.io") | Some("api.storyteller.io") => Some(".storyteller.io"),
_ => None,
};
// Kill the cookie
let mut cookie_builder = Cookie::build(STORYTELLER_DEMO_COOKIE_NAME, "")
.secure(server_state.env_config.cookie_secure) // HTTPS-only
.expires(OffsetDateTime::UNIX_EPOCH)
.http_only(false) // This is meant to be exposed to Javascript!
// NB: Since we're setting this from direct HTTP access rather than XHR, the browser
// implicitly sets the path used to access the cookie, ie. '/demo_mode', which is not what
// we want!
.path("/")
.permanent();
if let Some(cookie_domain) = maybe_cookie_domain {
cookie_builder = cookie_builder.domain(cookie_domain);
}
let mut cookie = cookie_builder.finish();
cookie.make_removal(); // Remove the cookie
HttpResponse::build(StatusCode::FOUND)
.append_header((header::LOCATION, safe_redirect_url.to_string()))
.cookie(cookie)
.finish()
}
@@ -1,67 +0,0 @@
use std::sync::Arc;
use actix_http::header;
use actix_web::cookie::Cookie;
use actix_web::http::StatusCode;
use actix_web::web::Query;
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use actix_helpers::extractors::get_request_host::get_request_host;
use crate::http_server::deprecated_endpoints::investor_demo::default_redirect::{redirect_is_allowed, DEFAULT_INVESTOR_REDIRECT};
use crate::http_server::deprecated_endpoints::investor_demo::demo_cookie::STORYTELLER_DEMO_COOKIE_NAME;
use crate::state::server_state::ServerState;
#[derive(Deserialize)]
pub struct QueryFields {
redirect_to: Option<String>,
}
pub async fn enable_demo_mode_handler(
http_request: HttpRequest,
query: Query<QueryFields>,
server_state: web::Data<Arc<ServerState>>
) -> impl Responder
{
let unsafe_redirect = query.redirect_to
.as_deref()
.map(|r| r.to_string())
.unwrap_or(DEFAULT_INVESTOR_REDIRECT.to_string());
let redirect_allowed = redirect_is_allowed(&unsafe_redirect);
let safe_redirect_url = if redirect_allowed {
unsafe_redirect
} else {
DEFAULT_INVESTOR_REDIRECT.to_string()
};
let maybe_host = get_request_host(&http_request);
let maybe_cookie_domain = match maybe_host {
Some("jungle.horse") | Some("api.jungle.horse") => Some(".jungle.horse"),
Some("fakeyou.com") | Some("api.fakeyou.com") => Some(".fakeyou.com"),
Some("storyteller.io") | Some("api.storyteller.io") => Some(".storyteller.io"),
_ => None,
};
// Add the cookie.
let mut cookie_builder = Cookie::build(STORYTELLER_DEMO_COOKIE_NAME, "true")
.secure(server_state.env_config.cookie_secure) // HTTPS-only
.http_only(false) // This is meant to be exposed to Javascript!
// NB: Since we're setting this from direct HTTP access rather than XHR, the browser
// implicitly sets the path used to access the cookie, ie. '/demo_mode', which is not what
// we want!
.path("/")
.permanent();
if let Some(cookie_domain) = maybe_cookie_domain {
cookie_builder = cookie_builder.domain(cookie_domain);
}
let cookie = cookie_builder.finish();
HttpResponse::build(StatusCode::FOUND)
.append_header((header::LOCATION, safe_redirect_url.to_string()))
.cookie(cookie)
.finish()
}
@@ -1,4 +0,0 @@
pub mod demo_cookie;
pub mod disable_demo_mode_handler;
pub mod enable_demo_mode_handler;
pub mod default_redirect;
@@ -1,255 +0,0 @@
// NB: Incrementally getting rid of build warnings...
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
use std::sync::Arc;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::{web, HttpRequest, HttpResponse};
use log::{error, info, warn};
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_extended::UserSessionExtended;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use enums::common::visibility::Visibility;
use http_server_common::request::get_request_header_optional::get_request_header_optional;
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{GenericInferenceArgs, InferenceCategoryAbbreviated, PolymorphicInferenceArgs};
use mysql_queries::payloads::generic_inference_args::inner_payloads::mocap_payload::{MocapArgs, MocapVideoSource};
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{insert_generic_inference_job, InsertGenericInferenceArgs};
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
/// Debug requests can get routed to special "debug-only" workers, which can
/// be used to trial new code, run debugging, etc.
const DEBUG_HEADER_NAME : &str = "enable-debug-mode";
/// The routing tag header can send workloads to particular k8s hosts.
/// This is useful for catching the live logs or intercepting the job.
const ROUTING_TAG_HEADER_NAME : &str = "routing-tag";
#[derive(Deserialize)]
pub struct EnqueueMocapnetRequest {
uuid_idempotency_token: String,
video_source: Option<MediaFileToken>,
creator_set_visibility: Option<Visibility>,
}
/// Treated as an enum. Only one of these may be set.
#[derive(Serialize)]
pub struct EnqueueMocapnetSuccessResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
#[derive(Debug)]
pub enum EnqueueMocapnetError {
BadInput(String),
NotAuthorized,
ServerError,
RateLimited,
}
impl ResponseError for EnqueueMocapnetError {
fn status_code(&self) -> StatusCode {
match *self {
EnqueueMocapnetError::BadInput(_) => StatusCode::BAD_REQUEST,
EnqueueMocapnetError::NotAuthorized => StatusCode::UNAUTHORIZED,
EnqueueMocapnetError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
EnqueueMocapnetError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
EnqueueMocapnetError::BadInput(reason) => reason.to_string(),
EnqueueMocapnetError::NotAuthorized => "unauthorized".to_string(),
EnqueueMocapnetError::ServerError => "server error".to_string(),
EnqueueMocapnetError::RateLimited => "rate limited".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl std::fmt::Display for EnqueueMocapnetError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
pub async fn enqueue_mocapnet_handler(
http_request: HttpRequest,
request: web::Json<EnqueueMocapnetRequest>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, EnqueueMocapnetError>
{
let mut maybe_user_token : Option<UserToken> = None;
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
warn!("MySql pool error: {:?}", err);
EnqueueMocapnetError::ServerError
})?;
let maybe_avt_token = server_state.avt_cookie_manager.get_avt_token_from_request(&http_request);
// ==================== USER SESSION ==================== //
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
EnqueueMocapnetError::ServerError
})?;
if let Some(user_session) = maybe_user_session.as_ref() {
maybe_user_token = Some(UserToken::new_from_str(&user_session.user_token));
}
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment,
maybe_user_session.as_ref());
// TODO: Separate priority for animation.
let priority_level = plan.web_vc_base_priority_level();
// ==================== DEBUG MODE + ROUTING TAG ==================== //
let is_debug_request =
get_request_header_optional(&http_request, DEBUG_HEADER_NAME)
.is_some();
let maybe_routing_tag=
get_request_header_optional(&http_request, ROUTING_TAG_HEADER_NAME)
.map(|routing_tag| routing_tag.trim().to_string());
// ==================== RATE LIMIT ==================== //
let rate_limiter = match maybe_user_session {
None => &server_state.redis_rate_limiters.logged_out,
Some(ref user) => {
if user.role.is_banned {
return Err(EnqueueMocapnetError::NotAuthorized);
}
&server_state.redis_rate_limiters.logged_in
},
};
if let Err(_err) = rate_limiter.rate_limit_request(&http_request).await {
return Err(EnqueueMocapnetError::RateLimited);
}
// ==================== HANDLE IDEMPOTENCY ==================== //
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(EnqueueMocapnetError::BadInput(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
EnqueueMocapnetError::BadInput("invalid idempotency token".to_string())
})?;
// ==================== LOOK UP MODEL INFO ==================== //
// TODO(bt): CHECK DATABASE FOR TOKENS!
let video_source: MocapVideoSource = request.video_source
.as_ref()
.map(|token| MocapVideoSource::media_file_token(token.as_str()))
.ok_or_else(|| EnqueueMocapnetError::BadInput("video source not fully specified".to_string()))?;
let ip_address = get_request_ip(&http_request);
let maybe_user_preferred_visibility : Option<Visibility> = maybe_user_session
.as_ref()
.map(|user_session: &UserSessionExtended| user_session.preferences.preferred_tts_result_visibility); // TODO: New setting for web-vc
let set_visibility = request.creator_set_visibility
.or(maybe_user_preferred_visibility)
.unwrap_or(Visibility::Public);
let inference_args = MocapArgs {
maybe_video_source: Some(video_source),
maybe_ik1: Some(0.001f32),
maybe_ik2: Some(99i32),
maybe_ik3: Some(99i32),
maybe_smoothing1: Some(60f32),
maybe_smoothing2: Some(10f32),
maybe_size1: Some(1920i32),
maybe_size2: Some(1080i32),
};
info!("Creating mocapnet job record...");
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
job_type: InferenceJobType::MocapNet,
maybe_product_category: None, // This is not a product anymore
inference_category: InferenceCategory::Mocap,
maybe_model_type: Some(InferenceModelType::MocapNet), // NB: Model is static during inference
maybe_model_token: None, // NB: Model is static during inference
maybe_input_source_token: None, // TODO: Introduce a second foreign key ?
maybe_input_source_token_type: None, // TODO: Introduce a second foreign key ?
maybe_download_url: None,
maybe_cover_image_media_file_token: None,
maybe_raw_inference_text: None, // No text
maybe_max_duration_seconds: None,
maybe_inference_args: Some(GenericInferenceArgs {
inference_category: Some(InferenceCategoryAbbreviated::Mocap),
args: Some(PolymorphicInferenceArgs::Mc(inference_args)),
}),
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: set_visibility,
priority_level,
requires_keepalive: plan.mocapnet_requires_frontend_keepalive(),
is_debug_request,
maybe_routing_tag: maybe_routing_tag.as_deref(),
mysql_pool: &server_state.mysql_pool, // TODO(bt,2024-02-04): Reuse connection
}).await;
let job_token = match query_result {
Ok((job_token, _id)) => job_token,
Err(err) => {
warn!("New generic inference job creation DB error: {:?}", err);
if err.had_duplicate_idempotency_token() {
return Err(EnqueueMocapnetError::BadInput("Duplicate idempotency token".to_string()));
}
return Err(EnqueueMocapnetError::ServerError);
}
};
let response: EnqueueMocapnetSuccessResponse = EnqueueMocapnetSuccessResponse {
success: true,
inference_job_token: job_token,
};
let body = serde_json::to_string(&response)
.map_err(|_e| EnqueueMocapnetError::ServerError)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}
@@ -1 +0,0 @@
pub(crate) mod enqueue_mocapnet;
@@ -6,10 +6,8 @@ pub mod engine;
pub mod events;
pub mod flags;
pub mod image_gen;
pub mod investor_demo;
pub mod leaderboard;
pub mod media_uploads;
pub mod mocap;
pub mod moderation;
pub mod stubs;
pub mod vocoders;
@@ -0,0 +1,62 @@
use log::warn;
use sqlx::{Executor, MySql};
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use tokens::tokens::users::UserToken;
/// Best-effort info-level debug log of an inbound generation HTTP request.
/// Never fails the request — insert errors are logged and swallowed.
pub async fn insert_generation_request_debug_log<'e, 'c: 'e, E>(
debug_log_event_token: &'e DebugLogEventToken,
user_token: &'e UserToken,
ip_address: &'e str,
request_url: &'e str,
request_json: &'e str,
mysql_executor: E,
) where
E: 'c + Executor<'c, Database = MySql>,
{
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,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(ip_address),
maybe_url: Some(request_url),
message: request_json,
mysql_executor,
phantom: Default::default(),
}).await {
warn!("Failed to insert HTTP request debug log: {:?}", err);
}
}
/// Best-effort error-level debug log of a generation backend failure.
/// Never fails the request — insert errors are logged and swallowed.
pub async fn insert_generation_failure_debug_log<'e, 'c: 'e, E>(
debug_log_event_token: &'e DebugLogEventToken,
user_token: &'e UserToken,
ip_address: &'e str,
request_url: &'e str,
error_message: &'e str,
mysql_executor: E,
) where
E: 'c + Executor<'c, Database = MySql>,
{
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::BackendFailure,
maybe_log_level: Some(DebugLogLevel::Error),
maybe_ip_address: Some(ip_address),
maybe_url: Some(request_url),
message: error_message,
mysql_executor,
phantom: Default::default(),
}).await {
warn!("Failed to insert generation failure debug log: {:?}", err);
}
}
@@ -1,4 +1,5 @@
mod test_synthetic_failure_reason;
pub mod generation_debug_logs;
pub mod job_failure_test;
pub mod map_seedance2pro_web_errors;
pub mod payments_error_test;
mod test_synthetic_failure_reason;
@@ -3,6 +3,9 @@ use std::sync::Arc;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::generate::common::generation_debug_logs::{
insert_generation_failure_debug_log, insert_generation_request_debug_log,
};
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
@@ -38,6 +41,7 @@ use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, g
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use utoipa::ToSchema;
/// Seedance 1.0 Lite Image to Video
@@ -84,6 +88,21 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
}
};
// ==================== DEBUG LOG: HTTP REQUEST ==================== //
let debug_log_event_token = DebugLogEventToken::generate();
let ip_address = get_request_ip(&http_request);
let request_url = http_request.uri().to_string();
insert_generation_request_debug_log(
&debug_log_event_token,
user_token,
&ip_address,
&request_url,
&serde_json::to_string(&*request).unwrap_or_default(),
&mut *mysql_connection,
).await;
let start_frame_media_file_token = match &request.media_file_token {
Some(token) => token,
None => {
@@ -208,12 +227,21 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
api_key: &server_state.inference_providers.fal.api_key,
};
let fal_result = enqueue_seedance_1_lite_image_to_video_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_seedance_1_lite_image_to_video_webhook: {:?}", err);
CommonWebError::from_error(err)
})?;
let fal_result = match enqueue_seedance_1_lite_image_to_video_webhook(args).await {
Ok(result) => result,
Err(err) => {
warn!("Error calling enqueue_seedance_1_lite_image_to_video_webhook: {:?}", err);
insert_generation_failure_debug_log(
&debug_log_event_token,
user_token,
&ip_address,
&request_url,
&format!("Seedance 1.0 Lite generation failed: {:?}", err),
&mut *mysql_connection,
).await;
return Err(CommonWebError::from_error(err));
}
};
let external_job_id = fal_result.request_id
.ok_or_else(|| {
@@ -223,8 +251,6 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
@@ -294,7 +320,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,
maybe_debug_log_event_token: Some(&debug_log_event_token),
phantom: Default::default(),
}).await;
@@ -3,6 +3,9 @@ use std::sync::Arc;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::generate::common::generation_debug_logs::{
insert_generation_failure_debug_log, insert_generation_request_debug_log,
};
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
@@ -35,6 +38,7 @@ use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, g
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use utoipa::ToSchema;
/// Seedance 1.0 Pro Image to Video
@@ -81,6 +85,21 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
}
};
// ==================== DEBUG LOG: HTTP REQUEST ==================== //
let debug_log_event_token = DebugLogEventToken::generate();
let ip_address = get_request_ip(&http_request);
let request_url = http_request.uri().to_string();
insert_generation_request_debug_log(
&debug_log_event_token,
user_token,
&ip_address,
&request_url,
&serde_json::to_string(&*request).unwrap_or_default(),
&mut *mysql_connection,
).await;
let start_frame_media_file_token = match &request.media_file_token {
Some(token) => token,
None => {
@@ -182,12 +201,21 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
api_key: &server_state.inference_providers.fal.api_key,
};
let fal_result = enqueue_seedance_1_pro_image_to_video_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_seedance_1_pro_image_to_video_webhook: {:?}", err);
CommonWebError::from_error(err)
})?;
let fal_result = match enqueue_seedance_1_pro_image_to_video_webhook(args).await {
Ok(result) => result,
Err(err) => {
warn!("Error calling enqueue_seedance_1_pro_image_to_video_webhook: {:?}", err);
insert_generation_failure_debug_log(
&debug_log_event_token,
user_token,
&ip_address,
&request_url,
&format!("Seedance 1.0 Pro generation failed: {:?}", err),
&mut *mysql_connection,
).await;
return Err(CommonWebError::from_error(err));
}
};
let external_job_id = fal_result.request_id
.ok_or_else(|| {
@@ -197,8 +225,6 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
@@ -268,7 +294,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,
maybe_debug_log_event_token: Some(&debug_log_event_token),
phantom: Default::default(),
}).await;
@@ -3,6 +3,9 @@ use std::sync::Arc;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::generate::common::generation_debug_logs::{
insert_generation_failure_debug_log, insert_generation_request_debug_log,
};
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
@@ -30,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 sqlx::Acquire;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
/// Seedance 1.5 Pro Multi-Function (text and image to video)
@@ -78,6 +82,21 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
}
};
// ==================== DEBUG LOG: HTTP REQUEST ==================== //
let debug_log_event_token = DebugLogEventToken::generate();
let ip_address = get_request_ip(&http_request);
let request_url = http_request.uri().to_string();
insert_generation_request_debug_log(
&debug_log_event_token,
user_token,
&ip_address,
&request_url,
&serde_json::to_string(&*request).unwrap_or_default(),
&mut *mysql_connection,
).await;
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
@@ -177,12 +196,21 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
api_key: &server_state.inference_providers.fal.api_key,
};
fal_result = enqueue_seedance_1p5_pro_image_to_video_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_seedance_1p5_pro_image_to_video_webhook: {:?}", err);
CommonWebError::from_error(err)
})?;
fal_result = match enqueue_seedance_1p5_pro_image_to_video_webhook(args).await {
Ok(result) => result,
Err(err) => {
warn!("Error calling enqueue_seedance_1p5_pro_image_to_video_webhook: {:?}", err);
insert_generation_failure_debug_log(
&debug_log_event_token,
user_token,
&ip_address,
&request_url,
&format!("Seedance 1.5 Pro generation failed: {:?}", err),
&mut *mysql_connection,
).await;
return Err(CommonWebError::from_error(err));
}
};
} else {
info!("text-to-video case");
@@ -217,12 +245,21 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
&mut mysql_connection,
).await?;
fal_result = enqueue_seedance_1p5_pro_text_to_video_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_seedance_1p5_pro_text_to_video_webhook: {:?}", err);
CommonWebError::from_error(err)
})?;
fal_result = match enqueue_seedance_1p5_pro_text_to_video_webhook(args).await {
Ok(result) => result,
Err(err) => {
warn!("Error calling enqueue_seedance_1p5_pro_text_to_video_webhook: {:?}", err);
insert_generation_failure_debug_log(
&debug_log_event_token,
user_token,
&ip_address,
&request_url,
&format!("Seedance 1.5 Pro generation failed: {:?}", err),
&mut *mysql_connection,
).await;
return Err(CommonWebError::from_error(err));
}
};
}
let external_job_id = fal_result.request_id
@@ -233,8 +270,6 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
@@ -342,7 +377,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,
maybe_debug_log_event_token: Some(&debug_log_event_token),
phantom: Default::default(),
}).await;
@@ -5,6 +5,12 @@ use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoint_helpers::refund_wallet_after_api_failure::refund_wallet_after_api_failure;
use crate::http_server::endpoints::generate::common::map_seedance2pro_web_errors::map_seedance2pro_error_to_web_error;
use crate::http_server::endpoints::generate::common::generation_debug_logs::{
insert_generation_failure_debug_log, insert_generation_request_debug_log,
};
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_feature_flags::UserSessionFeatureFlags;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
@@ -49,6 +55,9 @@ use sqlx::MySql;
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 tokens::tokens::users::UserToken;
use url::Url;
use url_utils::extension::extract_extension_from_url::{extract_extension_from_url, ExtractExtensions};
@@ -60,6 +69,14 @@ struct SeedanceGenerationResult {
generation_mode: CommonGenerationMode,
}
/// Request-scoped context for writing `debug_logs` rows.
struct DebugLogContext<'a> {
event_token: &'a DebugLogEventToken,
user_token: &'a UserToken,
ip_address: &'a str,
request_url: &'a str,
}
// ======================== Handler ========================
/// Seedance 2.0 Multi-Function video generation (text-to-video, keyframe, and reference).
@@ -108,6 +125,28 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
let user_token = &user_session.user_token;
// ==================== DEBUG LOG: HTTP REQUEST ==================== //
let debug_log_event_token = DebugLogEventToken::generate();
let ip_address = get_request_ip(&http_request);
let request_url = http_request.uri().to_string();
insert_generation_request_debug_log(
&debug_log_event_token,
user_token,
&ip_address,
&request_url,
&serde_json::to_string(&*request).unwrap_or_default(),
&mut *mysql_connection,
).await;
let debug_log_context = DebugLogContext {
event_token: &debug_log_event_token,
user_token,
ip_address: &ip_address,
request_url: &request_url,
};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
@@ -203,6 +242,8 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
batch_count,
duration_seconds,
kinovi_character_ids.clone(),
&debug_log_context,
&mut mysql_connection,
).await;
match result {
@@ -226,6 +267,8 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
batch_count,
duration_seconds,
kinovi_character_ids.clone(),
&debug_log_context,
&mut mysql_connection,
).await;
match result {
@@ -248,6 +291,14 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
}
}
insert_generation_failure_debug_log(
&debug_log_event_token,
user_token,
&ip_address,
&request_url,
&format!("Seedance 2.0 generation failed: {:?}", err),
&mut *mysql_connection,
).await;
refund_wallet_after_api_failure(&deduction_result.ledger_entry_token, &mut mysql_connection).await?;
return Err(err);
}
@@ -270,6 +321,8 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
batch_count,
duration_seconds,
kinovi_character_ids,
&debug_log_context,
&mut mysql_connection,
).await;
match result {
@@ -289,6 +342,14 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
}
}
insert_generation_failure_debug_log(
&debug_log_event_token,
user_token,
&ip_address,
&request_url,
&format!("Seedance 2.0 generation failed: {:?}", err),
&mut *mysql_connection,
).await;
refund_wallet_after_api_failure(&deduction_result.ledger_entry_token, &mut mysql_connection).await?;
return Err(err);
}
@@ -305,8 +366,6 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
// --- DB writes in a transaction ---
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
@@ -438,7 +497,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
maybe_debug_log_event_token: None,
maybe_debug_log_event_token: Some(&debug_log_event_token),
mysql_executor: &mut *transaction,
phantom: Default::default(),
}
@@ -543,6 +602,8 @@ async fn upload_and_generate(
batch_count: KinoviBatchCount,
duration_seconds: u8,
kinovi_character_ids: Option<Vec<String>>,
debug_log_context: &DebugLogContext<'_>,
mysql_connection: &mut sqlx::pool::PoolConnection<MySql>,
) -> Result<SeedanceGenerationResult, CommonWebError> {
// --- Upload files to seedance2pro CDN ---
@@ -605,24 +666,44 @@ async fn upload_and_generate(
let prompt = request.prompt.clone().unwrap_or_else(|| "".to_string());
let kinovi_request = KinoviGenerateVideoRequest {
model_type: KinoviModelType::Seedance2Pro,
prompt,
aspect_ratio,
output_resolution,
duration_seconds,
batch_count,
start_frame_url,
end_frame_url,
reference_image_urls,
reference_video_urls,
reference_audio_urls,
character_ids: kinovi_character_ids,
use_face_blur_hack: None,
};
// ==================== DEBUG LOG: KINOVI REQUEST ==================== //
// Logged BEFORE the send so the outbound payload is captured even when
// the generation call fails.
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
apriori_debug_log_event_token: Some(debug_log_context.event_token),
maybe_creator_user_token: Some(debug_log_context.user_token),
debug_log_type: DebugLogType::KinoviRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(debug_log_context.ip_address),
maybe_url: Some(debug_log_context.request_url),
message: &format!("{:#?}", kinovi_request),
mysql_executor: &mut **mysql_connection,
phantom: Default::default(),
}).await {
warn!("Failed to insert Kinovi request debug log: {:?}", err);
}
let video_gen_args = GenerateVideoArgs {
session,
host_override: None,
request: KinoviGenerateVideoRequest {
model_type: KinoviModelType::Seedance2Pro,
prompt,
aspect_ratio,
output_resolution,
duration_seconds,
batch_count,
start_frame_url,
end_frame_url,
reference_image_urls,
reference_video_urls,
reference_audio_urls,
character_ids: kinovi_character_ids,
use_face_blur_hack: None,
},
request: kinovi_request,
};
let gen_response = generate_video(video_gen_args).await
@@ -22,7 +22,6 @@ pub mod omni_gen;
pub mod prompts;
pub mod service;
pub mod stats;
pub mod studio_gen2;
pub mod subscriptions;
pub mod tags;
pub mod trending;
@@ -33,9 +32,6 @@ pub mod user_referral_codes;
pub mod user_wallets;
pub mod users;
pub mod video_info;
pub mod voice_clone_requests;
pub mod voice_conversion;
pub mod voice_designer;
pub mod web_referrals;
pub mod webhooks;
pub mod weights;
@@ -1 +1,3 @@
pub mod moderation_list_all_debug_logs_handler;
pub mod moderation_list_debug_logs_for_token_handler;
pub mod moderation_list_debug_logs_for_user_handler;
@@ -0,0 +1,91 @@
use std::sync::Arc;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use log::warn;
use artcraft_api_defs::moderation::debug_logs::debug_log_entry::ModerationDebugLogUser;
use artcraft_api_defs::moderation::debug_logs::moderation_list_all_debug_logs::{
ModerationListAllDebugLogsEntry, ModerationListAllDebugLogsQueryParams,
ModerationListAllDebugLogsSuccessResponse,
};
use mysql_queries::queries::debug_logs::list_all_debug_logs::{
list_all_debug_logs, ListAllDebugLogsArgs,
};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::user_lookup::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
#[utoipa::path(
get,
tag = "Moderation",
path = "/v1/moderation/debug_logs/list_all",
params(
ModerationListAllDebugLogsQueryParams,
),
responses(
(status = 200, description = "Success", body = ModerationListAllDebugLogsSuccessResponse),
(status = 400, description = "Bad request (invalid severity)"),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
)]
pub async fn moderation_list_all_debug_logs_handler(
http_request: HttpRequest,
query: web::Query<ModerationListAllDebugLogsQueryParams>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModerationListAllDebugLogsSuccessResponse>, CommonWebError> {
let maybe_log_levels = query.parsed_severities().map_err(|err| {
CommonWebError::BadInputWithSimpleMessage(format!("invalid severities parameter: {}", err))
})?;
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &mut *mysql_connection).await?;
let result = list_all_debug_logs(ListAllDebugLogsArgs {
maybe_log_levels,
maybe_id_cursor: query.cursor,
limit: query.limit,
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
}).await.map_err(|err| {
warn!("Error listing all debug logs: {:?}", err);
CommonWebError::from_error(err)
})?;
let debug_logs: Vec<ModerationListAllDebugLogsEntry> = result.debug_logs.into_iter().map(|row| {
// The join only yields user fields when the creator user exists.
let maybe_user = match (row.maybe_creator_user_token.clone(), row.maybe_user_display_name, row.maybe_user_username, row.maybe_user_gravatar_hash) {
(Some(user_token), Some(display_name), Some(username), Some(gravatar_hash)) => {
Some(ModerationDebugLogUser {
user_token,
display_name,
username,
gravatar_hash,
})
}
_ => None,
};
ModerationListAllDebugLogsEntry {
id: row.id,
event_token: row.event_token,
debug_log_type: row.debug_log_type,
maybe_log_level: row.maybe_log_level,
maybe_creator_user_token: row.maybe_creator_user_token,
maybe_ip_address: row.maybe_ip_address,
maybe_url: row.maybe_url,
message: row.message,
created_at: row.created_at,
maybe_user,
}
}).collect();
Ok(Json(ModerationListAllDebugLogsSuccessResponse {
success: true,
debug_logs,
next_cursor: result.next_cursor,
}))
}
@@ -2,80 +2,45 @@ 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 artcraft_api_defs::moderation::debug_logs::debug_log_entry::{ModerationDebugLogEntry, ModerationDebugLogUser};
use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_token::{
ModerationListDebugLogsForTokenPathInfo, ModerationListDebugLogsForTokenQueryParams,
ModerationListDebugLogsForTokenSuccessResponse,
};
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::common_web_error::CommonWebError;
use crate::http_server::user_lookup::user_session::require_moderator::require_moderator;
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,
ModerationListDebugLogsForTokenQueryParams,
),
responses(
(status = 200, description = "Success", body = ListDebugLogsSuccessResponse),
(status = 200, description = "Success", body = ModerationListDebugLogsForTokenSuccessResponse),
(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>,
path: web::Path<ModerationListDebugLogsForTokenPathInfo>,
query: web::Query<ModerationListDebugLogsForTokenQueryParams>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListDebugLogsSuccessResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
) -> Result<Json<ModerationListDebugLogsForTokenSuccessResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &mut *mysql_connection).await?;
let rows = list_debug_logs_for_token(ListDebugLogsForTokenArgs {
event_token: &path.token,
limit: query.limit,
@@ -86,18 +51,35 @@ pub async fn moderation_list_debug_logs_for_token_handler(
CommonWebError::from_error(err)
})?;
let debug_logs: Vec<DebugLogEntry> = rows.into_iter().map(|row| {
DebugLogEntry {
let debug_logs: Vec<ModerationDebugLogEntry> = rows.into_iter().map(|row| {
// The join only yields user fields when the creator user exists.
let maybe_user = match (row.maybe_creator_user_token.clone(), row.maybe_user_display_name, row.maybe_user_username, row.maybe_user_gravatar_hash) {
(Some(user_token), Some(display_name), Some(username), Some(gravatar_hash)) => {
Some(ModerationDebugLogUser {
user_token,
display_name,
username,
gravatar_hash,
})
}
_ => None,
};
ModerationDebugLogEntry {
id: row.id,
event_token: row.event_token,
debug_log_type: row.debug_log_type,
maybe_log_level: row.maybe_log_level,
maybe_creator_user_token: row.maybe_creator_user_token,
maybe_ip_address: row.maybe_ip_address,
maybe_url: row.maybe_url,
message: row.message,
created_at: row.created_at,
maybe_user,
}
}).collect();
Ok(Json(ListDebugLogsSuccessResponse {
Ok(Json(ModerationListDebugLogsForTokenSuccessResponse {
success: true,
debug_logs,
}))
@@ -0,0 +1,88 @@
use std::sync::Arc;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use log::warn;
use artcraft_api_defs::moderation::debug_logs::debug_log_entry::{ModerationDebugLogEntry, ModerationDebugLogUser};
use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_user::{
ModerationListDebugLogsForUserPathInfo, ModerationListDebugLogsForUserQueryParams,
ModerationListDebugLogsForUserSuccessResponse,
};
use mysql_queries::queries::debug_logs::list_debug_logs_for_user::{
list_debug_logs_for_user, ListDebugLogsForUserArgs,
};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::user_lookup::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
#[utoipa::path(
get,
tag = "Moderation",
path = "/v1/moderation/debug_logs/user_list/{user_token}",
params(
("user_token" = String, Path, description = "User token"),
ModerationListDebugLogsForUserQueryParams,
),
responses(
(status = 200, description = "Success", body = ModerationListDebugLogsForUserSuccessResponse),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
)]
pub async fn moderation_list_debug_logs_for_user_handler(
http_request: HttpRequest,
path: web::Path<ModerationListDebugLogsForUserPathInfo>,
query: web::Query<ModerationListDebugLogsForUserQueryParams>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModerationListDebugLogsForUserSuccessResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &mut *mysql_connection).await?;
let result = list_debug_logs_for_user(ListDebugLogsForUserArgs {
user_token: &path.user_token,
maybe_id_cursor: query.cursor,
limit: query.limit,
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
}).await.map_err(|err| {
warn!("Error listing debug logs for user {}: {:?}", path.user_token, err);
CommonWebError::from_error(err)
})?;
let debug_logs: Vec<ModerationDebugLogEntry> = result.debug_logs.into_iter().map(|row| {
// The join only yields user fields when the creator user exists.
let maybe_user = match (row.maybe_creator_user_token.clone(), row.maybe_user_display_name, row.maybe_user_username, row.maybe_user_gravatar_hash) {
(Some(user_token), Some(display_name), Some(username), Some(gravatar_hash)) => {
Some(ModerationDebugLogUser {
user_token,
display_name,
username,
gravatar_hash,
})
}
_ => None,
};
ModerationDebugLogEntry {
id: row.id,
event_token: row.event_token,
debug_log_type: row.debug_log_type,
maybe_log_level: row.maybe_log_level,
maybe_creator_user_token: row.maybe_creator_user_token,
maybe_ip_address: row.maybe_ip_address,
maybe_url: row.maybe_url,
message: row.message,
created_at: row.created_at,
maybe_user,
}
}).collect();
Ok(Json(ModerationListDebugLogsForUserSuccessResponse {
success: true,
debug_logs,
next_cursor: result.next_cursor,
}))
}
@@ -16,6 +16,7 @@ use enums::common::generation::common_model_type::CommonModelType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::platform_type::PlatformType;
use http_server_common::request::get_request_ip::get_request_ip;
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
use mysql_queries::queries::generic_inference::api_providers::seedance2pro::insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token::KinoviVersion;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
@@ -106,6 +107,7 @@ pub async fn omni_api_image_generate_handler(
})?;
let ip_address = get_request_ip(&http_request);
let request_url = http_request.uri().to_string();
// ==================== INGEST URL INPUTS ==================== //
@@ -138,6 +140,9 @@ pub async fn omni_api_image_generate_handler(
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::HttpRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &serde_json::to_string(&*request).unwrap_or_default(),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
@@ -157,7 +162,32 @@ pub async fn omni_api_image_generate_handler(
server_state: &server_state,
user_token,
resolved_media: &resolved_media,
}).await?;
}).await;
// ==================== DEBUG LOG: PIPELINE ERROR ==================== //
let pipeline_result = match pipeline_result {
Ok(result) => result,
Err(err) => {
// Best-effort error log; never mask the original error.
if let Ok(mut error_log_connection) = server_state.mysql_pool.acquire().await {
if let Err(log_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::BackendFailure,
maybe_log_level: Some(DebugLogLevel::Error),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("Image generation pipeline failed: {:?}", err),
mysql_executor: &mut *error_log_connection,
phantom: Default::default(),
}).await {
warn!("Failed to insert pipeline error debug log: {:?}", log_err);
}
}
return Err(err);
}
};
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
@@ -169,6 +199,9 @@ pub async fn omni_api_image_generate_handler(
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::FalRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("{:#?}", outbound_request),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
@@ -18,6 +18,7 @@ use enums::common::generation::common_video_model::CommonVideoModel;
use enums::common::generation_provider::GenerationProvider;
use enums::common::platform_type::PlatformType;
use http_server_common::request::get_request_ip::get_request_ip;
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
use mysql_queries::queries::generic_inference::api_providers::seedance2pro::insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token::KinoviVersion;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
@@ -119,6 +120,7 @@ pub async fn omni_api_video_generate_handler(
})?;
let ip_address = get_request_ip(&http_request);
let request_url = http_request.uri().to_string();
// ==================== INGEST URL INPUTS ==================== //
@@ -205,6 +207,9 @@ pub async fn omni_api_video_generate_handler(
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::HttpRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &serde_json::to_string(&*request).unwrap_or_default(),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
@@ -224,7 +229,32 @@ pub async fn omni_api_video_generate_handler(
media_file_to_url_map: &media_file_to_url_map,
kinovi_character_id_map: &kinovi_character_id_map,
kinovi_account,
}).await?;
}).await;
// ==================== DEBUG LOG: PIPELINE ERROR ==================== //
let pipeline_result = match pipeline_result {
Ok(result) => result,
Err(err) => {
// Best-effort error log; never mask the original error.
if let Ok(mut error_log_connection) = server_state.mysql_pool.acquire().await {
if let Err(log_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::BackendFailure,
maybe_log_level: Some(DebugLogLevel::Error),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("Video generation pipeline failed: {:?}", err),
mysql_executor: &mut *error_log_connection,
phantom: Default::default(),
}).await {
warn!("Failed to insert pipeline error debug log: {:?}", log_err);
}
}
return Err(err);
}
};
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
@@ -236,6 +266,9 @@ pub async fn omni_api_video_generate_handler(
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::FalRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("{:#?}", outbound_request),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
@@ -253,6 +286,9 @@ pub async fn omni_api_video_generate_handler(
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::GrokApiRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("{:#?}", outbound_request),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
@@ -15,6 +15,7 @@ use enums::common::generation::common_generation_mode::CommonGenerationMode;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::generation_provider::GenerationProvider;
use http_server_common::request::get_request_ip::get_request_ip;
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
use mysql_queries::queries::generic_inference::api_providers::seedance2pro::insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token::KinoviVersion;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
@@ -130,10 +131,16 @@ pub async fn omni_gen_image_generate_handler(
// ==================== DEBUG LOG: HTTP REQUEST ==================== //
let ip_address = get_request_ip(&http_request);
let request_url = http_request.uri().to_string();
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,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &serde_json::to_string(&*request).unwrap_or_default(),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
@@ -153,7 +160,32 @@ pub async fn omni_gen_image_generate_handler(
server_state: &server_state,
user_token,
resolved_media: &resolved_media,
}).await?;
}).await;
// ==================== DEBUG LOG: PIPELINE ERROR ==================== //
let pipeline_result = match pipeline_result {
Ok(result) => result,
Err(err) => {
// Best-effort error log; never mask the original error.
if let Ok(mut error_log_connection) = server_state.mysql_pool.acquire().await {
if let Err(log_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::BackendFailure,
maybe_log_level: Some(DebugLogLevel::Error),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("Image generation pipeline failed: {:?}", err),
mysql_executor: &mut *error_log_connection,
phantom: Default::default(),
}).await {
warn!("Failed to insert pipeline error debug log: {:?}", log_err);
}
}
return Err(err);
}
};
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
@@ -165,6 +197,9 @@ pub async fn omni_gen_image_generate_handler(
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::FalRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("{:#?}", outbound_request),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
@@ -176,7 +211,6 @@ pub async fn omni_gen_image_generate_handler(
// ==================== WRITE RESULT ==================== //
let ip_address = get_request_ip(&http_request);
let maybe_platform_type = get_request_platform_type(&http_request);
let mut transaction = mysql_connection
@@ -17,6 +17,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 enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
use mysql_queries::queries::generic_inference::api_providers::seedance2pro::insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token::KinoviVersion;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
@@ -191,10 +192,16 @@ pub async fn omni_gen_video_generate_handler(
// ==================== DEBUG LOG: HTTP REQUEST ==================== //
let ip_address = get_request_ip(&http_request);
let request_url = http_request.uri().to_string();
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,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &serde_json::to_string(&*request).unwrap_or_default(),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
@@ -214,7 +221,32 @@ pub async fn omni_gen_video_generate_handler(
media_file_to_url_map: &media_file_to_url_map,
kinovi_character_id_map: &kinovi_character_id_map,
kinovi_account,
}).await?;
}).await;
// ==================== DEBUG LOG: PIPELINE ERROR ==================== //
let pipeline_result = match pipeline_result {
Ok(result) => result,
Err(err) => {
// Best-effort error log; never mask the original error.
if let Ok(mut error_log_connection) = server_state.mysql_pool.acquire().await {
if let Err(log_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::BackendFailure,
maybe_log_level: Some(DebugLogLevel::Error),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("Video generation pipeline failed: {:?}", err),
mysql_executor: &mut *error_log_connection,
phantom: Default::default(),
}).await {
warn!("Failed to insert pipeline error debug log: {:?}", log_err);
}
}
return Err(err);
}
};
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
@@ -226,6 +258,9 @@ pub async fn omni_gen_video_generate_handler(
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::FalRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("{:#?}", outbound_request),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
@@ -243,6 +278,9 @@ pub async fn omni_gen_video_generate_handler(
apriori_debug_log_event_token: Some(&debug_log_event_token),
maybe_creator_user_token: Some(user_token),
debug_log_type: DebugLogType::GrokApiRequest,
maybe_log_level: Some(DebugLogLevel::Info),
maybe_ip_address: Some(&ip_address),
maybe_url: Some(&request_url),
message: &format!("{:#?}", outbound_request),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
@@ -254,7 +292,6 @@ pub async fn omni_gen_video_generate_handler(
// ==================== WRITE RESULT ==================== //
let ip_address = get_request_ip(&http_request);
let maybe_platform_type = get_request_platform_type(&http_request);
let mut transaction = mysql_connection.begin().await.map_err(|err| {
@@ -1,298 +0,0 @@
// NB: Incrementally getting rid of build warnings...
// #![forbid(unused_imports)]
// #![forbid(unused_mut)]
// #![forbid(unused_variables)]
use std::sync::Arc;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::{web, HttpRequest, HttpResponse};
use log::{error, info, warn};
use utoipa::ToSchema;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::visibility::Visibility;
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::payloads::generic_inference_args::common::watermark_type::WatermarkType;
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{GenericInferenceArgs, InferenceCategoryAbbreviated, PolymorphicInferenceArgs};
use mysql_queries::payloads::generic_inference_args::inner_payloads::studio_gen2_payload::StudioGen2Payload;
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{insert_generic_inference_job, InsertGenericInferenceArgs};
use mysql_queries::queries::generic_inference::web::kill_jobs_in_development::kill_jobs_in_development;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::configs::plans::plan_category::PlanCategory;
use crate::http_server::requests::request_headers::get_routing_tag_header::get_routing_tag_header;
use crate::http_server::requests::request_headers::has_debug_header::has_debug_header;
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_extended::UserSessionExtended;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use crate::util::allowed_video_style_transfer_access::allowed_video_style_transfer_access;
#[derive(Deserialize, ToSchema)]
pub struct EnqueueStudioGen2Request {
/// Entropy for request de-duplication (required)
pub uuid_idempotency_token: String,
/// The input image media file (required)
pub image_file: MediaFileToken,
/// The input video media file (required)
pub video_file: MediaFileToken,
/// Remove watermark from the output
/// Only for premium accounts
pub remove_watermark: Option<bool>,
/// Optional visibility setting override.
pub creator_set_visibility: Option<Visibility>,
/// Sleep for debugging
pub debug_sleep_millis: Option<u64>,
/// Kill old jobs (only in development)
pub debug_kill_old_jobs: Option<bool>,
// TODO
pub output_width: Option<u64>,
pub output_height: Option<u64>,
pub fps: Option<u64>,
// TODO
pub trim_duration_millis: Option<u64>,
// TODO
pub max_frames: Option<u64>,
pub rounds: Option<u64>,
pub skip_image_resize: Option<bool>,
/// Model image tensor width (we do not resize images to this!)
pub tensor_image_width: Option<u64>,
/// Model image tensor height (we do not resize images to this!)
pub tensor_image_height: Option<u64>,
}
#[derive(Serialize, ToSchema)]
pub struct EnqueueStudioGen2Response {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
/// Enqueue Studio Gen2 jobs.
#[utoipa::path(
post,
tag = "Studio Gen2",
path = "/v1/studio_gen2/enqueue",
responses(
(status = 200, description = "Success", body = EnqueueStudioGen2Response),
(status = 400, description = "Bad input", body = CommonWebError),
(status = 401, description = "Not authorized", body = CommonWebError),
(status = 429, description = "Rate limited", body = CommonWebError),
(status = 500, description = "Server error", body = CommonWebError)
),
params(("request" = EnqueueStudioGen2Request, description = "Payload for request"))
)]
pub async fn enqueue_studio_gen2_handler(
http_request: HttpRequest,
request: Json<EnqueueStudioGen2Request>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<EnqueueStudioGen2Response>, CommonWebError> {
let a = 1;
// ==================== DB ==================== //
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
warn!("MySql pool error: {:?}", err);
CommonWebError::from_error(err)
})?;
let maybe_avt_token = server_state.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// ==================== USER SESSION ==================== //
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::from_error(e)
})?;
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.user_token_typed.clone());
// ==================== FEATURE FLAG CHECK ==================== //
//if !allowed_video_style_transfer_access(maybe_user_session.as_ref(), &server_state.flags) {
// warn!("Video style transfer access is not permitted for user");
// return Err(CommonWebError::NotAuthorized);
//}
// ==================== PAID PLAN + PRIORITY ==================== //
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment,
maybe_user_session.as_ref());
// TODO: Separate priority for animation.
let priority_level = plan.web_vc_base_priority_level();
let is_staff = maybe_user_session
.as_ref()
.map(|user| user.role.can_ban_users)
.unwrap_or(false);
// ==================== DEBUG MODE + ROUTING TAG ==================== //
let is_debug_request = has_debug_header(&http_request);
let maybe_routing_tag= get_routing_tag_header(&http_request);
// ==================== RATE LIMIT ==================== //
let rate_limiter = match maybe_user_session {
None => &server_state.redis_rate_limiters.logged_out,
Some(ref user) => {
if user.role.is_banned {
return Err(CommonWebError::NotAuthorized);
}
&server_state.redis_rate_limiters.logged_in
},
};
if let Err(_err) = rate_limiter.rate_limit_request(&http_request).await {
return Err(CommonWebError::TooManyRequests);
}
// ==================== HANDLE IDEMPOTENCY ==================== //
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
CommonWebError::BadInputWithSimpleMessage("invalid idempotency token".to_string())
})?;
// NB: The connection isn't needed past this point (the enqueue below uses the pool directly),
// so release it now rather than holding a pool slot for the rest of the handler.
drop(mysql_connection);
// ==================== LOOK UP MODEL INFO ==================== //
// TODO(bt): CHECK DATABASE FOR TOKENS!
let ip_address = get_request_ip(&http_request);
let maybe_user_preferred_visibility : Option<Visibility> = maybe_user_session
.as_ref()
.map(|user_session: &UserSessionExtended| user_session.preferences.preferred_tts_result_visibility); // TODO: New setting for web-vc
let set_visibility = request.creator_set_visibility
.or(maybe_user_preferred_visibility)
.unwrap_or(Visibility::Public);
let has_paid_plan = plan.plan_slug() == "fakeyou_contributor" || plan.plan_category() == PlanCategory::Paid;
let watermark_type ;
let is_requested_to_remove_watermark = request.remove_watermark.unwrap_or(false);
let is_allowed_to_remove_watermark = is_staff; // TODO: Paid permission instead
if is_requested_to_remove_watermark && is_allowed_to_remove_watermark {
watermark_type = None;
} else {
watermark_type = Some(WatermarkType::Storyteller); // Studio is only on Storyteller.
}
if request.debug_kill_old_jobs.unwrap_or(false)
&& server_state.server_environment.is_development()
{
info!("Killing old jobs before inserting new job (DEVELOPMENT MODE ONLY!)");
let result = kill_jobs_in_development(&server_state.mysql_pool).await;
if let Err(err) = result {
warn!("Error killing old jobs: {:?}", err);
}
}
let inference_args = StudioGen2Payload {
image_file: Some(request.image_file.clone()),
video_file: Some(request.video_file.clone()),
creator_visibility: Some(set_visibility),
watermark_type,
after_job_debug_sleep_millis: request.debug_sleep_millis,
output_width: request.output_width,
output_height: request.output_height,
fps: request.fps,
max_frames: request.max_frames,
rounds: request.rounds,
trim_duration_millis: request.trim_duration_millis,
skip_image_resize: request.skip_image_resize,
tensor_image_width: request.tensor_image_width,
tensor_image_height: request.tensor_image_height,
};
info!("Creating ComfyUI job record...");
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
job_type: InferenceJobType::StudioGen2,
inference_category: InferenceCategory::DeprecatedField,
maybe_model_type: None,
maybe_product_category: None,
maybe_model_token: None,
maybe_input_source_token: None, // TODO: Introduce a second foreign key ?
maybe_input_source_token_type: None, // TODO: Introduce a second foreign key ?
maybe_download_url: None,
maybe_cover_image_media_file_token: None,
maybe_raw_inference_text: None, // No text
maybe_max_duration_seconds: None,
maybe_inference_args: Some(GenericInferenceArgs {
inference_category: Some(InferenceCategoryAbbreviated::Workflow),
args: Some(PolymorphicInferenceArgs::S2(inference_args)),
}),
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: set_visibility,
priority_level,
requires_keepalive: false,
is_debug_request,
maybe_routing_tag: maybe_routing_tag.as_deref(),
mysql_pool: &server_state.mysql_pool,
}).await;
let job_token = match query_result {
Ok((job_token, _id)) => job_token,
Err(err) => {
warn!("New generic inference job creation DB error: {:?}", err);
if err.had_duplicate_idempotency_token() {
return Err(CommonWebError::BadInputWithSimpleMessage("Duplicate idempotency token".to_string()));
}
return Err(CommonWebError::from_error(err));
}
};
Ok(Json(EnqueueStudioGen2Response {
success: true,
inference_job_token: job_token,
}))
}
@@ -1 +0,0 @@
pub mod enqueue_studio_gen2_handler;
@@ -1,524 +0,0 @@
// NB: Incrementally getting rid of build warnings...
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
use std::fmt;
use std::sync::Arc;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::{web, HttpRequest, HttpResponse};
use log::{info, warn};
use rand::seq::IndexedRandom;
use rand::Rng;
use utoipa::ToSchema;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::by_table::generic_inference_jobs::inference_input_source_token_type::InferenceInputSourceTokenType;
use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::visibility::Visibility;
use http_server_common::request::get_request_api_token::get_request_api_token;
use http_server_common::request::get_request_header_optional::get_request_header_optional;
use http_server_common::request::get_request_ip::get_request_ip;
use migration::text_to_speech::get_tts_model_for_enqueue_inference_migration::TtsModelForEnqueueInferenceMigrationWrapper;
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{GenericInferenceArgs, InferenceCategoryAbbreviated, PolymorphicInferenceArgs};
use mysql_queries::payloads::generic_inference_args::inner_payloads::f5_tts_payload::F5TTSPayload;
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{insert_generic_inference_job, InsertGenericInferenceArgs};
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::media_uploads::MediaUploadToken;
use tokens::tokens::users::UserToken;
use tts_common::priority::{FAKEYOU_DEFAULT_VALID_API_TOKEN_PRIORITY_LEVEL, FAKEYOU_INVESTOR_PRIORITY_LEVEL};
use user_input_common::check_for_slurs::contains_slurs;
use crate::configs::app_startup::username_set::UsernameSet;
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
use crate::configs::plans::plan::Plan;
use crate::http_server::deprecated_endpoints::investor_demo::demo_cookie::request_has_demo_cookie;
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_extended::UserSessionExtended;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
// TODO: Temporary for investor demo
const STORYTELLER_DEMO_COOKIE_NAME : &str = "storyteller_demo";
/// Debug requests mean different things, depending on the type of worker (the legacy
/// tts-inference-job vs. the new inference-job):
///
/// * Legacy (tts): these requests get routed to special "debug-only" workers, which can
/// be used to trial new code, run debugging, etc.
///
/// * New (generic): the flag gets associated with the request which may be used for
/// anything in the code. (There's a new system for routing requests called
/// "routing tags" that is independent of this flag.)
///
const DEBUG_HEADER_NAME : &str = "enable-debug-mode";
/// The routing tag header can send workloads to particular k8s hosts.
/// This is useful for catching the live logs or intercepting the job.
/// NB: This is only for the new job system.
const ROUTING_TAG_HEADER_NAME : &str = "routing-tag";
const USER_FAKEYOU_USER_TOKEN : &str = "U:N5J8JXPW9BTYX";
const USER_NEWS_STORY_USER_TOKEN : &str = "U:XAWRARC1N89X6";
/// Safety guard against plans supporting too lengthy TTS inference text.
const MAX_TTS_LENGTH : usize = 10_000;
#[derive(Deserialize, ToSchema)]
pub struct InferTtsRequest {
uuid_idempotency_token: String,
source_media_file_token: String,
inference_text: String,
creator_set_visibility: Option<Visibility>,
}
#[derive(Serialize, ToSchema)]
pub struct InferTtsSuccessResponse {
pub success: bool,
pub inference_job_token: String,
}
#[derive(Debug, ToSchema)]
pub enum InferTtsError {
BadInput(String),
NotAuthorized,
ServerError,
RateLimited,
NotFound,
}
impl ResponseError for InferTtsError {
fn status_code(&self) -> StatusCode {
match *self {
InferTtsError::BadInput(_) => StatusCode::BAD_REQUEST,
InferTtsError::NotAuthorized => StatusCode::UNAUTHORIZED,
InferTtsError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
InferTtsError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
InferTtsError::NotFound => StatusCode::NOT_FOUND,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
InferTtsError::BadInput(reason) => reason.to_string(),
InferTtsError::NotAuthorized => "unauthorized".to_string(),
InferTtsError::ServerError => "server error".to_string(),
InferTtsError::RateLimited => "rate limited".to_string(),
InferTtsError::NotFound => "not found".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for InferTtsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[utoipa::path(
post,
tag = "TTS",
path = "/v1/tts/f5_inference",
responses(
(status = 200, description = "Success response", body = InferTtsSuccessResponse),
(status = 400, description = "Bad input", body = InferTtsError),
(status = 401, description = "Not authorized", body = InferTtsError),
(status = 429, description = "Rate limited", body = InferTtsError),
(status = 500, description = "Server error", body = InferTtsError)
),
params(("request" = InferTtsRequest, description = "Payload for Request"))
)]
pub async fn enqueue_infer_f5_tts_handler(
http_request: HttpRequest,
request: Json<InferTtsRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<InferTtsSuccessResponse>, InferTtsError> {
if server_state.flags.disable_tts {
return Err(InferTtsError::RateLimited);
}
let mut is_from_api = false;
let mut maybe_user_token : Option<String> = None;
let mut priority_level ;
let mut use_high_priority_rate_limiter = false; // NB: Careful!
let mut disable_rate_limiter = false; // NB: Careful!
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
warn!("MySql pool error: {:?}", err);
InferTtsError::ServerError
})?;
// ==================== USER SESSION ==================== //
let maybe_user_session : Option<UserSessionExtended> = server_state
.session_checker
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
InferTtsError::ServerError
})?;
if let Some(user_session) = maybe_user_session.as_ref() {
maybe_user_token = Some(user_session.user_token.to_string());
}
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment,
maybe_user_session.as_ref());
priority_level = plan.tts_base_priority_level();
// ==================== API TOKENS ==================== //
if let Some(api_token) = get_request_api_token(&http_request) {
let maybe_api_token_configs = server_state
.static_api_token_set
.get_api_token(&api_token);
if let Some(api_token_configs) = maybe_api_token_configs {
is_from_api = true;
priority_level = api_token_configs.maybe_priority_level
.unwrap_or(FAKEYOU_DEFAULT_VALID_API_TOKEN_PRIORITY_LEVEL);
use_high_priority_rate_limiter = api_token_configs.maybe_use_high_priority_rate_limiter
.unwrap_or(false);
disable_rate_limiter = api_token_configs.maybe_disable_rate_limiter.unwrap_or(false);
if let Some(user_token_override) = api_token_configs.maybe_user_token.as_deref() {
maybe_user_token = Some(user_token_override.trim().to_string());
}
}
}
// ==================== INVESTOR PRIORITY ==================== //
// TODO/TEMP: Give investors even more priority
let mut is_investor = false;
// TODO/TEMP: The storyteller.io website will redirect and establish this cookie.
// This is just for the YCombinator demo.
if request_has_demo_cookie(&http_request) {
is_investor = true;
}
if is_investor {
priority_level = FAKEYOU_INVESTOR_PRIORITY_LEVEL;
}
// ==================== ENGINEERING FLAGS / HEADERS: DEBUG MODE, ETC. ==================== //
let is_debug_request = get_request_header_optional(&http_request, DEBUG_HEADER_NAME)
.is_some();
let maybe_routing_tag=
get_request_header_optional(&http_request, ROUTING_TAG_HEADER_NAME)
.map(|routing_tag| routing_tag.trim().to_string());
// ==================== BANNED USERS ==================== //
if let Some(ref user) = maybe_user_session {
if user.role.is_banned {
warn!("User is not authorized to use TTS model because they are banned.");
return Err(InferTtsError::NotAuthorized);
}
}
// ==================== RATE LIMIT ==================== //
if !disable_rate_limiter {
let maybe_username = maybe_user_session
.as_ref()
.map(|session| session.user.username.as_str());
// FIXME(bt,2023-12-13): These boolean flags are a bit dangerous.
let rate_limiter_type = get_rate_limiter_type(
maybe_username,
&server_state.redis_rate_limiters.api_ai_streamer_username_set,
is_investor,
is_from_api,
use_high_priority_rate_limiter
);
let rate_limiter = match rate_limiter_type {
RateLimiterType::LoggedOut => &server_state.redis_rate_limiters.logged_out,
RateLimiterType::LoggedIn => &server_state.redis_rate_limiters.logged_in,
RateLimiterType::ApiHighPriority => {
info!("Using API high priority rate limiter");
&server_state.redis_rate_limiters.api_high_priority
},
RateLimiterType::ApiAiStreamer => {
info!("Using AI streamer rate limiter");
&server_state.redis_rate_limiters.api_ai_streamers
},
};
if let Err(_err) = rate_limiter.rate_limit_request(&http_request).await {
return Err(InferTtsError::RateLimited);
}
}
// let is_authorized = check_if_authorized_to_use_media_file(
// maybe_user_session.as_ref(),
// &tts_model,
// ).await;
//
// if !is_authorized {
// warn!("User is not authorized to use TTS model due to checks.");
// return Err(InferTtsError::NotAuthorized);
// }
//
// ==================== CHECK INFERENCE TEXT ==================== //
let ip_address = get_request_ip(&http_request);
let mut inference_text = request.inference_text.trim().to_string();
if let Err(reason) = validate_inference_text(&inference_text, &plan) {
return Err(InferTtsError::BadInput(reason));
}
if contains_slurs(&inference_text) {
return Err(InferTtsError::BadInput("text contains slurs".to_string()));
}
if let Some(user) = maybe_user_session.as_ref() {
let is_user_token_troll_banned = server_state.troll_bans.user_tokens
.contains_user_token(&user.user_token)
.unwrap_or(false); // NB: Fail open
let is_ip_address_troll_banned = server_state.troll_bans.ip_addresses
.contains_ip_address(&ip_address)
.unwrap_or(false); // NB: Fail open
let is_troll_banned = is_user_token_troll_banned || is_ip_address_troll_banned;
if is_troll_banned {
let test = server_state.flags.troll_ban_user_percent as i32; // NB: Should be 0 - 100.
let num = rand::thread_rng().gen_range(0..100);
if test > num {
warn!("Generating troll response for user token: {}", user.user_token);
inference_text = random_troll_text().to_string();
}
}
}
let maybe_user_preferred_visibility : Option<Visibility> = maybe_user_session
.as_ref()
.map(|user_session| user_session.preferences.preferred_tts_result_visibility);
let set_visibility = request.creator_set_visibility
.or(maybe_user_preferred_visibility)
.unwrap_or(Visibility::Public);
// ==================== ENQUEUE APPROPRIATE TTS TYPE ==================== //
let max_duration_seconds = plan.tts_max_duration_seconds();
// This branch uses the `generic-inference-job` service and tables.
info!("Creating tts inference job record (new generic job system)...");
let maybe_creator_user_token_typed = maybe_user_token
.as_deref()
.map(|token| UserToken::new_from_str(token));
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
let maybe_model_type = None;
let maybe_inference_args = Some(GenericInferenceArgs{
inference_category: Some(InferenceCategoryAbbreviated::F5TTS),
args: Some(PolymorphicInferenceArgs::Ft(F5TTSPayload{})),
});
let media_token = request.source_media_file_token.clone();
let media_token_type =
if media_token.starts_with(MediaUploadToken::token_prefix()) {
InferenceInputSourceTokenType::MediaUpload
} else if media_token.starts_with(MediaFileToken::token_prefix()) {
InferenceInputSourceTokenType::MediaFile
} else {
return Err(InferTtsError::BadInput(
"input token is not a media_upload or media_file token".to_string()));
};
let maybe_product_category = Some(InferenceJobProductCategory::TtsF5);
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
job_type: InferenceJobType::F5TTS,
maybe_product_category,
inference_category: InferenceCategory::F5TTS,
maybe_model_type,
maybe_model_token: None,
maybe_input_source_token: Some(&media_token),
maybe_input_source_token_type: Some(media_token_type),
maybe_download_url: None,
maybe_cover_image_media_file_token: None,
maybe_raw_inference_text: Some(inference_text.as_str()),
maybe_max_duration_seconds: Some(max_duration_seconds),
maybe_inference_args,
maybe_creator_user_token: maybe_creator_user_token_typed.as_ref(),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: set_visibility,
priority_level,
requires_keepalive: false, // TODO: We may want this to be the case in the future.
is_debug_request,
maybe_routing_tag: maybe_routing_tag.as_deref(),
mysql_pool: &server_state.mysql_pool,
}).await;
let inference_job_token = match query_result {
Ok((inference_job_token, _id)) => inference_job_token,
Err(err) => {
warn!("New (generic) tts inference job creation DB error: {:?}", err);
if err.had_duplicate_idempotency_token() {
return Err(InferTtsError::BadInput("Duplicate idempotency token".to_string()));
}
return Err(InferTtsError::ServerError);
}
};
let job_token = inference_job_token.to_string();
Ok(Json(InferTtsSuccessResponse {
success: true,
inference_job_token: job_token,
}))
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum RateLimiterType {
LoggedOut,
LoggedIn,
ApiHighPriority,
ApiAiStreamer,
}
// FIXME(bt,2023-12-13): The boolean flags and disjoint decision making are kind of terrible.
fn get_rate_limiter_type(
maybe_username: Option<&str>,
ai_streamer_username_set: &UsernameSet,
is_investor: bool,
is_from_api: bool,
use_high_priority_rate_limiter: bool,
) -> RateLimiterType {
// For AI live streamers
// https://www.notion.so/storytellerai/dd88609558a24196b4ddeeef6079da98
let mut is_ai_streamer = false;
let mut rate_limiter = match maybe_username {
None => RateLimiterType::LoggedOut,
Some(username) => {
if ai_streamer_username_set.username_is_in_set(username) {
is_ai_streamer = true;
}
RateLimiterType::LoggedIn
},
};
// TODO/TEMP
if is_investor || is_from_api {
rate_limiter = RateLimiterType::LoggedIn;
}
// TODO: This is for VidVoice.ai and should be replaced with per-API consumer rate limiters
if use_high_priority_rate_limiter {
rate_limiter = RateLimiterType::ApiHighPriority;
}
if is_ai_streamer {
rate_limiter = RateLimiterType::ApiAiStreamer;
}
rate_limiter
}
pub fn validate_inference_text(text: &str, plan: &Plan) -> Result<(), String> {
if text.len() < 3 {
return Err("text is too short".to_string());
}
if text.len() > plan.tts_max_character_length() {
return Err("text is too long".to_string());
}
if text.len() > MAX_TTS_LENGTH {
return Err("text is too long".to_string());
}
Ok(())
}
/// If the model is private, determine if the user can use the model.
/// This is designed to fail closed (read: actually open!) rather than hit resources.
async fn check_if_authorized_to_use_model(
maybe_user_session: Option<&UserSessionExtended>,
tts_model: &TtsModelForEnqueueInferenceMigrationWrapper,
) -> bool {
match tts_model.creator_set_visibility() {
Visibility::Public => return true,
Visibility::Hidden => return true,
Visibility::Private => {} // Fall through
}
let is_authorized = maybe_user_session
.map(|session| {
(session.user_token.as_str() == tts_model.creator_user_token()) // is_author
|| (session.role.can_ban_users) // is_mod
|| (session.user_token.as_str() == USER_FAKEYOU_USER_TOKEN) // is allowed user #1
|| (session.user_token.as_str() == USER_NEWS_STORY_USER_TOKEN) // is allowed user #2
})
.unwrap_or(false);
is_authorized
}
fn random_troll_text() -> &'static str {
let texts = [
"Fake You Dot Com. Use Fake You Dot Com. The website Fake You Dot Com has better voices. Use Fake You Dot Com.",
"Use Fake You Dot Com. Fake You Dot Com. Fake You Dot Com is way better.",
"Want better voices? Fake. You. Fake You Dot Com. Fake. Fake. Fake. You. You. You. Use Fake You Dot Com. Fake You Dot Com.",
r#"
You should use Fake You Dot Com. Fake You Dot Com has way better voices.
You can get everything you want from FakeYou dot com. One Oh One Sound Boards website is just using Fake You.
Use Fake You Dot Com. Fake You Dot Com. Fake You Dot com.
AAAAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
"#,
//"EEEEEEEEEEEEEEEEE",
//"One Oh One Soundboards is a shitty website. Use Fake You Dot Com instead.",
//r#"
//beep. beep. beep. beep. beep.
//May have installed a virus
//OOOOOOOOEEEEUUUUUOOAAAA
//Your device may be compromised
//EEEEEEEEEEEEEEEEIIIUUUUOOOAAAA
//Your location may be tracked by the advanced artificial intelligence.
//"#,
];
let random = texts.choose(&mut rand::thread_rng());
random.map(|r| *r).unwrap_or(texts[0])
}
@@ -1 +0,0 @@
pub mod enqueue_infer_f5_tts_handler;
@@ -36,7 +36,6 @@ use user_input_common::check_for_slurs::contains_slurs;
use crate::configs::app_startup::username_set::UsernameSet;
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
use crate::configs::plans::plan::Plan;
use crate::http_server::deprecated_endpoints::investor_demo::demo_cookie::request_has_demo_cookie;
use crate::http_server::endpoints::tts::enqueue_infer_tts_handler::get_tts_model_with_caching::get_tts_model_with_caching;
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_extended::UserSessionExtended;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
@@ -235,12 +234,6 @@ pub async fn enqueue_infer_tts_handler(
_ => {},
};
// TODO/TEMP: The storyteller.io website will redirect and establish this cookie.
// This is just for the YCombinator demo.
if request_has_demo_cookie(&http_request) {
is_investor = true;
}
if is_investor {
priority_level = FAKEYOU_INVESTOR_PRIORITY_LEVEL;
}
@@ -1,120 +1,42 @@
use std::fmt;
use std::sync::Arc;
use actix_web::HttpResponse;
use chrono::Utc;
use log::error;
use actix_web::{http::StatusCode, web, HttpRequest, HttpResponse, ResponseError};
use chrono::NaiveDateTime;
use log::{debug, error, warn};
use mysql_queries::queries::tts::tts_inference_jobs::get_pending_tts_inference_job_count::get_pending_tts_inference_job_count;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::state::server_state::ServerState;
/// How fast the frontend should poll this endpoint. Effectively "never" —
/// the endpoint is retired and only zombie traffic hits it.
const REFRESH_INTERVAL_MILLIS: u64 = 1_000_000;
#[derive(Serialize)]
pub struct Response {
pub success: bool,
pub pending_job_count: u64,
pub cache_time: NaiveDateTime,
pub cache_time: chrono::NaiveDateTime,
/// Tell the frontend client how fast to refresh their view of this list.
/// During an attack, we may want this to go extremely slow.
pub refresh_interval_millis: u64,
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
pub async fn get_pending_tts_inference_job_count_handler(
http_request: HttpRequest,
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, CommonWebError> {
if server_state.flags.disable_tts_queue_length_endpoint {
// NB: Despite the cache being a powerful protector of the database (this is an expensive query),
// if the cache goes stale during an outage, there is no protection. This feature flag lets us
// shut off all traffic to the endpoint.
return render_response_busy(Response {
success: true,
pending_job_count: 10_000,
cache_time: NaiveDateTime::from_timestamp(0, 0),
refresh_interval_millis: server_state.flags.frontend_pending_tts_refresh_interval_millis,
});
}
let maybe_cached = server_state.caches.ephemeral.tts_queue_length.grab_copy_without_bump_if_unexpired()
.map_err(|e| {
error!("error consulting cache: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
let count_result = match maybe_cached {
Some(cached) => {
cached
},
None => {
debug!("populating tts queue length from database");
let count_query_result = get_pending_tts_inference_job_count(
&server_state.mysql_pool
).await;
match count_query_result {
// If the database misbehaves (eg. DDoS), let's stop spamming it.
// We'll attempt to read the old value from the cache and keep going.
Err(err) => {
warn!("error querying database / inserting into cache: {:?}", err);
let maybe_cached = server_state.caches.ephemeral.tts_queue_length.grab_even_expired_and_bump()
.map_err(|err| {
error!("error consulting cache (even expired): {:?}", err);
CommonWebError::from_anyhow_error(err)
})?;
maybe_cached.ok_or_else(|| {
error!("error querying database and subsequently reading cache: {:?}", err);
CommonWebError::server_error_with_message("uncaught server error")
})?
}
// Happy path...
Ok(count_result) => {
server_state.caches.ephemeral.tts_queue_length.store_copy(&count_result)
.map_err(|e| {
error!("error storing cache: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
count_result
}
}
},
/// RETIRED endpoint that still receives heavy zombie traffic (~300 rpm).
///
/// The TTS queue no longer exists, so this returns a hardcoded zero without
/// touching the cache or the database.
pub async fn get_pending_tts_inference_job_count_handler() -> Result<HttpResponse, CommonWebError> {
let response = Response {
success: true,
pending_job_count: 0,
cache_time: Utc::now().naive_utc(),
refresh_interval_millis: REFRESH_INTERVAL_MILLIS,
};
render_response_ok(Response {
success: true,
pending_job_count: count_result.record_count,
cache_time: count_result.present_time,
refresh_interval_millis: server_state.flags.frontend_pending_tts_refresh_interval_millis,
})
}
let body = serde_json::to_string(&response)
.map_err(|e| {
error!("error returning response: {:?}", e);
CommonWebError::from_error(e)
})?;
pub fn render_response_busy(response: Response) -> Result<HttpResponse, CommonWebError> {
let body = render_response_payload(response)?;
Ok(HttpResponse::TooManyRequests()
.content_type("application/json")
.body(body))
}
pub fn render_response_ok(response: Response) -> Result<HttpResponse, CommonWebError> {
let body = render_response_payload(response)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}
pub fn render_response_payload(response: Response) -> Result<String, CommonWebError> {
let body = serde_json::to_string(&response)
.map_err(|e| {
error!("error returning response: {:?}", e);
CommonWebError::from_error(e)
})?;
Ok(body)
}
@@ -13,5 +13,4 @@ pub mod get_tts_upload_model_job_status;
pub mod list_tts_models;
pub mod list_user_tts_inference_results;
pub mod list_user_tts_models;
pub mod search_tts_models_handler;
pub mod enqueue_infer_f5_tts_handler;
pub mod search_tts_models_handler;
@@ -1,88 +0,0 @@
use std::fmt;
use std::sync::Arc;
use actix_web::error::ResponseError;
use actix_web::web::Json;
use actix_web::http::StatusCode;
use actix_web::{web, HttpRequest};
use log::warn;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use mysql_queries::queries::voice_clone_requests::get_voice_clone_request::{get_voice_clone_request_by_token, get_voice_clone_request_by_user_token};
use crate::state::server_state::ServerState;
use crate::http_server::common_responses::common_web_error::CommonWebError;
// =============== Request ===============
#[derive(Deserialize)]
pub struct CheckIfVoiceRequestSubmittedRequest {
/// Though we look up logged in users by user token, we can also look up non-logged-in users
/// with a token lookup. We can store this in frontend state or a cookie.
pub maybe_request_token: Option<String>,
}
// =============== Success Response ===============
#[derive(Serialize)]
pub struct CheckIfVoiceRequestSubmittedResponse {
pub success: bool,
pub has_submitted: bool,
}
// =============== Error Response ===============
// NB: Not using DeriveMore since Clion doesn't understand it.
// =============== Handler ===============
pub async fn check_if_voice_clone_request_submitted_handler(
http_request: HttpRequest,
request: Json<CheckIfVoiceRequestSubmittedRequest>,
server_state: web::Data<Arc<ServerState>>) -> Result<Json<CheckIfVoiceRequestSubmittedResponse>, CommonWebError>
{
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::from_error(e)
})?;
let mut submitted_by_token = false;
let mut submitted_by_user = false;
// Non-logged in accounts (via cookie or frontend mechanism)
if let Some(token) = request.maybe_request_token.as_deref() {
let request = get_voice_clone_request_by_token(
token, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Database error: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
submitted_by_token = request.is_some();
}
// Logged in users
if let Some(user) = maybe_user_session.as_ref() {
let request = get_voice_clone_request_by_user_token(
user.user_token.as_str(), &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Database error: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
submitted_by_user = request.is_some();
}
let has_submitted = submitted_by_token || submitted_by_user;
let response = CheckIfVoiceRequestSubmittedResponse {
success: true,
has_submitted,
};
Ok(Json(response))
}
@@ -1,118 +0,0 @@
use std::fmt;
use std::sync::Arc;
use actix_web::error::ResponseError;
use actix_web::web::Json;
use actix_web::http::StatusCode;
use actix_web::{web, HttpRequest};
use log::warn;
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use mysql_queries::queries::voice_clone_requests::insert_voice_clone_requests_builder::InsertVoiceCloneRequestBuilder;
use crate::state::server_state::ServerState;
use crate::http_server::common_responses::common_web_error::CommonWebError;
// =============== Request ===============
#[derive(Deserialize)]
pub struct CreateVoiceRequestRequest {
pub idempotency_token: String,
// Contact
pub email_address: String,
pub discord_username: String,
// TODO: Make enum.
// Visibility
pub is_for_private_use: bool,
pub is_for_public_use: bool,
// Use
pub is_for_studio: bool,
pub is_for_twitch_tts: bool,
pub is_for_api_use: bool,
pub is_for_music: bool,
pub is_for_games: bool,
pub is_for_other: bool,
pub optional_notes_on_use: Option<String>,
// Subject/Ownership
pub is_own_voice: bool,
pub is_third_party_voice: bool,
pub optional_notes_on_subject: Option<String>,
// Equipment
pub has_clean_audio_recordings: bool,
pub has_good_microphone: bool,
// Comments
pub optional_questions: Option<String>,
pub optional_extra_comments: Option<String>,
}
// =============== Success Response ===============
#[derive(Serialize)]
pub struct CreateVoiceRequestResponse {
pub success: bool,
}
// =============== Error Response ===============
// NB: Not using DeriveMore since Clion doesn't understand it.
// =============== Handler ===============
pub async fn create_voice_clone_request_handler(
http_request: HttpRequest,
request: Json<CreateVoiceRequestRequest>,
server_state: web::Data<Arc<ServerState>>) -> Result<Json<CreateVoiceRequestResponse>, CommonWebError>
{
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::from_error(e)
})?;
let creator_ip_address = get_request_ip(&http_request);
let builder = InsertVoiceCloneRequestBuilder {
uuid_idempotency_token: request.idempotency_token.clone(),
maybe_user_token: maybe_user_session.map(|user| user.user_token.as_str().to_string()),
email_address: request.email_address.clone(),
discord_username: request.discord_username.clone(),
is_for_private_use: request.is_for_private_use,
is_for_public_use: request.is_for_public_use,
is_for_studio: request.is_for_studio,
is_for_twitch_tts: request.is_for_twitch_tts,
is_for_api_use: request.is_for_api_use,
is_for_music: request.is_for_music,
is_for_games: request.is_for_games,
is_for_other: request.is_for_other,
optional_notes_on_use: request.optional_notes_on_use.clone(),
is_own_voice: request.is_own_voice,
is_third_party_voice: request.is_third_party_voice,
optional_notes_on_subject: request.optional_notes_on_subject.clone(),
has_clean_audio_recordings: request.has_clean_audio_recordings,
has_good_microphone: request.has_good_microphone,
optional_questions: request.optional_questions.clone(),
optional_extra_comments: request.optional_extra_comments.clone(),
ip_address_creation: creator_ip_address,
};
builder.insert(&server_state.mysql_pool)
.await
.map_err(|e| {
warn!("creation error: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
let response = CreateVoiceRequestResponse {
success: true,
};
Ok(Json(response))
}
@@ -1,2 +0,0 @@
pub mod create_voice_clone_request;
pub mod check_if_voice_clone_request_submitted;
@@ -1,358 +0,0 @@
// NB: Incrementally getting rid of build warnings...
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
use std::sync::Arc;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use log::{info, warn};
use utoipa::ToSchema;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::by_table::generic_inference_jobs::inference_input_source_token_type::InferenceInputSourceTokenType;
use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::visibility::Visibility;
use http_server_common::request::get_request_api_token::get_request_api_token;
use http_server_common::request::get_request_header_optional::get_request_header_optional;
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{GenericInferenceArgs, InferenceCategoryAbbreviated, PolymorphicInferenceArgs};
use mysql_queries::payloads::generic_inference_args::inner_payloads::seed_vc_payload::SeedVcPayload;
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{insert_generic_inference_job, InsertGenericInferenceArgs};
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::media_uploads::MediaUploadToken;
use tokens::tokens::users::UserToken;
use tts_common::priority::{FAKEYOU_DEFAULT_VALID_API_TOKEN_PRIORITY_LEVEL, FAKEYOU_INVESTOR_PRIORITY_LEVEL};
use crate::configs::app_startup::username_set::UsernameSet;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
use crate::http_server::deprecated_endpoints::investor_demo::demo_cookie::request_has_demo_cookie;
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_extended::UserSessionExtended;
use crate::state::server_state::ServerState;
// TODO: Temporary for investor demo
const STORYTELLER_DEMO_COOKIE_NAME : &str = "storyteller_demo";
///
const DEBUG_HEADER_NAME : &str = "enable-debug-mode";
/// The routing tag header can send workloads to particular k8s hosts.
/// This is useful for catching the live logs or intercepting the job.
/// NB: This is only for the new job system.
const ROUTING_TAG_HEADER_NAME : &str = "routing-tag";
const USER_FAKEYOU_USER_TOKEN : &str = "U:N5J8JXPW9BTYX";
const USER_NEWS_STORY_USER_TOKEN : &str = "U:XAWRARC1N89X6";
#[derive(Deserialize, ToSchema)]
pub struct InferSeedVcRequest {
uuid_idempotency_token: String,
source_media_file_token: String,
reference_media_file_token: String,
creator_set_visibility: Option<Visibility>,
}
#[derive(Serialize, ToSchema)]
pub struct InferSeedVcSuccessResponse {
pub success: bool,
pub inference_job_token: String,
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
#[utoipa::path(
post,
tag = "Voice Conversion",
path = "/v1/voice_conversion/seed_vc_inference",
responses(
(status = 200, description = "Success response", body = InferSeedVcSuccessResponse),
(status = 400, description = "Bad input", body = CommonWebError),
(status = 401, description = "Not authorized", body = CommonWebError),
(status = 429, description = "Rate limited", body = CommonWebError),
(status = 500, description = "Server error", body = CommonWebError)
),
params(("request" = InferSeedVcRequest, description = "Payload for Request"))
)]
pub async fn enqueue_infer_seed_vc_handler(
http_request: HttpRequest,
request: Json<InferSeedVcRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<InferSeedVcSuccessResponse>, CommonWebError> {
if server_state.flags.disable_voice_conversion {
return Err(CommonWebError::TooManyRequests);
}
let mut is_from_api = false;
let mut maybe_user_token : Option<String> = None;
let mut priority_level ;
let mut use_high_priority_rate_limiter = false; // NB: Careful!
let mut disable_rate_limiter = false; // NB: Careful!
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
warn!("MySql pool error: {:?}", err);
CommonWebError::from_error(err)
})?;
// ==================== USER SESSION ==================== //
let maybe_user_session : Option<UserSessionExtended> = server_state
.session_checker
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::from_error(e)
})?;
if let Some(user_session) = maybe_user_session.as_ref() {
maybe_user_token = Some(user_session.user_token.to_string());
}
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment,
maybe_user_session.as_ref());
priority_level = plan.tts_base_priority_level();
// ==================== API TOKENS ==================== //
if let Some(api_token) = get_request_api_token(&http_request) {
let maybe_api_token_configs = server_state
.static_api_token_set
.get_api_token(&api_token);
if let Some(api_token_configs) = maybe_api_token_configs {
is_from_api = true;
priority_level = api_token_configs.maybe_priority_level
.unwrap_or(FAKEYOU_DEFAULT_VALID_API_TOKEN_PRIORITY_LEVEL);
use_high_priority_rate_limiter = api_token_configs.maybe_use_high_priority_rate_limiter
.unwrap_or(false);
disable_rate_limiter = api_token_configs.maybe_disable_rate_limiter.unwrap_or(false);
if let Some(user_token_override) = api_token_configs.maybe_user_token.as_deref() {
maybe_user_token = Some(user_token_override.trim().to_string());
}
}
}
// ==================== INVESTOR PRIORITY ==================== //
// TODO/TEMP: Give investors even more priority
let mut is_investor = false;
// TODO/TEMP: The storyteller.io website will redirect and establish this cookie.
// This is just for the YCombinator demo.
if request_has_demo_cookie(&http_request) {
is_investor = true;
}
if is_investor {
priority_level = FAKEYOU_INVESTOR_PRIORITY_LEVEL;
}
// ==================== ENGINEERING FLAGS / HEADERS: DEBUG MODE, ETC. ==================== //
let is_debug_request = get_request_header_optional(&http_request, DEBUG_HEADER_NAME)
.is_some();
let maybe_routing_tag=
get_request_header_optional(&http_request, ROUTING_TAG_HEADER_NAME)
.map(|routing_tag| routing_tag.trim().to_string());
// ==================== BANNED USERS ==================== //
if let Some(ref user) = maybe_user_session {
if user.role.is_banned {
warn!("User is not authorized to use Seed-VC because they are banned.");
return Err(CommonWebError::NotAuthorized);
}
}
// ==================== RATE LIMIT ==================== //
if !disable_rate_limiter {
let maybe_username = maybe_user_session
.as_ref()
.map(|session| session.user.username.as_str());
// FIXME(bt,2023-12-13): These boolean flags are a bit dangerous.
let rate_limiter_type = get_rate_limiter_type(
maybe_username,
&server_state.redis_rate_limiters.api_ai_streamer_username_set,
is_investor,
is_from_api,
use_high_priority_rate_limiter
);
let rate_limiter = match rate_limiter_type {
RateLimiterType::LoggedOut => &server_state.redis_rate_limiters.logged_out,
RateLimiterType::LoggedIn => &server_state.redis_rate_limiters.logged_in,
RateLimiterType::ApiHighPriority => {
info!("Using API high priority rate limiter");
&server_state.redis_rate_limiters.api_high_priority
},
RateLimiterType::ApiAiStreamer => {
info!("Using AI streamer rate limiter");
&server_state.redis_rate_limiters.api_ai_streamers
},
};
if let Err(_err) = rate_limiter.rate_limit_request(&http_request).await {
return Err(CommonWebError::TooManyRequests);
}
}
let ip_address = get_request_ip(&http_request);
let maybe_user_preferred_visibility : Option<Visibility> = maybe_user_session
.as_ref()
.map(|user_session| user_session.preferences.preferred_tts_result_visibility);
let set_visibility = request.creator_set_visibility
.or(maybe_user_preferred_visibility)
.unwrap_or(Visibility::Public);
let maybe_creator_user_token_typed = maybe_user_token
.as_deref()
.map(|token| UserToken::new_from_str(token));
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
let maybe_model_type = None;
let source_media_token = request.source_media_file_token.clone();
let source_media_token_type =
if source_media_token.starts_with(MediaUploadToken::token_prefix()) {
InferenceInputSourceTokenType::MediaUpload
} else if source_media_token.starts_with(MediaFileToken::token_prefix()) {
InferenceInputSourceTokenType::MediaFile
} else {
return Err(CommonWebError::BadInputWithSimpleMessage(
"input token is not a media_upload or media_file token".to_string()));
};
let reference_media_token = request.reference_media_file_token.clone();
let _reference_media_token_type =
if reference_media_token.starts_with(MediaUploadToken::token_prefix()) {
InferenceInputSourceTokenType::MediaUpload
} else if reference_media_token.starts_with(MediaFileToken::token_prefix()) {
InferenceInputSourceTokenType::MediaFile
} else {
return Err(CommonWebError::BadInputWithSimpleMessage(
"reference token is not a media_upload or media_file token".to_string()));
};
let maybe_inference_args = Some(GenericInferenceArgs{
inference_category: Some(InferenceCategoryAbbreviated::SeedVc),
args: Some(PolymorphicInferenceArgs::Sv(SeedVcPayload{
reference_media_file_token: Option::from(MediaFileToken::new_from_str(&reference_media_token)),
})),
});
let maybe_product_category = Some(InferenceJobProductCategory::VcSeedVc);
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
job_type: InferenceJobType::SeedVc,
maybe_product_category,
inference_category: InferenceCategory::SeedVc,
maybe_model_type,
maybe_model_token: None,
maybe_input_source_token: Some(&source_media_token),
maybe_input_source_token_type: Some(source_media_token_type),
maybe_download_url: None,
maybe_cover_image_media_file_token: None,
maybe_raw_inference_text: None,
maybe_max_duration_seconds: Some(30),
maybe_inference_args,
maybe_creator_user_token: maybe_creator_user_token_typed.as_ref(),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: set_visibility,
priority_level,
requires_keepalive: false, // TODO: We may want this to be the case in the future.
is_debug_request,
maybe_routing_tag: maybe_routing_tag.as_deref(),
mysql_pool: &server_state.mysql_pool,
}).await;
let inference_job_token = match query_result {
Ok((inference_job_token, _id)) => inference_job_token,
Err(err) => {
warn!("New (generic) seed-vc inference job creation DB error: {:?}", err);
if err.had_duplicate_idempotency_token() {
return Err(CommonWebError::BadInputWithSimpleMessage("Duplicate idempotency token".to_string()));
}
return Err(CommonWebError::from_error(err));
}
};
let job_token = inference_job_token.to_string();
Ok(Json(InferSeedVcSuccessResponse {
success: true,
inference_job_token: job_token,
}))
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum RateLimiterType {
LoggedOut,
LoggedIn,
ApiHighPriority,
ApiAiStreamer,
}
// FIXME(bt,2023-12-13): The boolean flags and disjoint decision making are kind of terrible.
fn get_rate_limiter_type(
maybe_username: Option<&str>,
ai_streamer_username_set: &UsernameSet,
is_investor: bool,
is_from_api: bool,
use_high_priority_rate_limiter: bool,
) -> RateLimiterType {
// For AI live streamers
// https://www.notion.so/storytellerai/dd88609558a24196b4ddeeef6079da98
let mut is_ai_streamer = false;
let mut rate_limiter = match maybe_username {
None => RateLimiterType::LoggedOut,
Some(username) => {
if ai_streamer_username_set.username_is_in_set(username) {
is_ai_streamer = true;
}
RateLimiterType::LoggedIn
},
};
// TODO/TEMP
if is_investor || is_from_api {
rate_limiter = RateLimiterType::LoggedIn;
}
// TODO: This is for VidVoice.ai and should be replaced with per-API consumer rate limiters
if use_high_priority_rate_limiter {
rate_limiter = RateLimiterType::ApiHighPriority;
}
if is_ai_streamer {
rate_limiter = RateLimiterType::ApiAiStreamer;
}
rate_limiter
}
@@ -1,423 +0,0 @@
// NB: Incrementally getting rid of build warnings...
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
use std::sync::Arc;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use log::{error, info, warn};
use redis::Commands;
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use utoipa::ToSchema;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::by_table::generic_inference_jobs::inference_input_source_token_type::InferenceInputSourceTokenType;
use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use enums::common::visibility::Visibility;
use http_server_common::request::get_request_header_optional::get_request_header_optional;
use http_server_common::request::get_request_ip::get_request_ip;
use migration::voice_conversion::query_vc_model_info_lite_for_migration::query_vc_model_info_lite_for_migration_with_connection;
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{FundamentalFrequencyMethodForJob, GenericInferenceArgs, InferenceCategoryAbbreviated, PolymorphicInferenceArgs};
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{insert_generic_inference_job, InsertGenericInferenceArgs};
use redis_common::redis_keys::RedisKeys;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::media_uploads::MediaUploadToken;
use tokens::tokens::users::UserToken;
use tts_common::priority::FAKEYOU_INVESTOR_PRIORITY_LEVEL;
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::deprecated_endpoints::investor_demo::demo_cookie::request_has_demo_cookie;
use crate::state::memory_cache::model_token_to_info_cache::{ModelInfoForInferenceJob, ModelTokenToInfoCache};
use crate::state::server_state::ServerState;
/// Debug requests can get routed to special "debug-only" workers, which can
/// be used to trial new code, run debugging, etc.
const DEBUG_HEADER_NAME : &str = "enable-debug-mode";
/// The routing tag header can send workloads to particular k8s hosts.
/// This is useful for catching the live logs or intercepting the job.
const ROUTING_TAG_HEADER_NAME : &str = "routing-tag";
#[derive(Deserialize, ToSchema)]
pub struct EnqueueVoiceConversionInferenceRequest {
uuid_idempotency_token: String,
/// Model weights token for the voice conversion model.
/// This must be either a SoVitsSvc or RvcV2 model.
voice_conversion_model_token: String,
/// NB: This can be a `MediaUploadToken` or a `MediaFileToken` token.
/// We will eventually migrate everything to media files and remove
/// media uploads (legacy, deprecated).
source_media_upload_token: String,
creator_set_visibility: Option<Visibility>,
is_storyteller_demo: Option<bool>,
/// Argument for so-vits-svc
/// The python model defaults to true, but that sounds awful,
/// so we default to false unless specified.
auto_predict_f0: Option<bool>,
/// Argument for so-vits-svc
/// f0 estimation
override_f0_method: Option<FundamentalFrequencyMethod>,
/// Argument for so-vits-svc
/// Pitch controls for so-vits-svc
transpose: Option<i32>,
}
#[derive(Deserialize, Clone, Copy, ToSchema)]
pub enum FundamentalFrequencyMethod {
/// RMVPE is the best algorithm as of 2023-10-10.
#[serde(rename = "rmvpe")]
Rmvpe,
#[serde(rename = "crepe")]
Crepe,
#[serde(rename = "dio")]
Dio,
#[serde(rename = "harvest")]
Harvest,
}
#[derive(Serialize, ToSchema)]
pub struct EnqueueVoiceConversionInferenceSuccessResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
#[utoipa::path(
post,
tag = "Voice Conversion",
path = "/v1/voice_conversion/inference",
responses(
(status = 200, description = "Success", body = EnqueueVoiceConversionInferenceSuccessResponse),
(status = 400, description = "Bad input", body = CommonWebError),
(status = 401, description = "Not authorized", body = CommonWebError),
(status = 429, description = "Rate limited", body = CommonWebError),
(status = 500, description = "Server error", body = CommonWebError),
),
params(
("request" = EnqueueVoiceConversionInferenceRequest, description = "Payload for Request"),
)
)]
pub async fn enqueue_voice_conversion_inference_handler(
http_request: HttpRequest,
request: Json<EnqueueVoiceConversionInferenceRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<EnqueueVoiceConversionInferenceSuccessResponse>, CommonWebError>
{
if server_state.flags.disable_voice_conversion {
return Err(CommonWebError::TooManyRequests);
}
let mut maybe_user_token : Option<UserToken> = None;
let mut priority_level ;
let disable_rate_limiter = false; // NB: Careful!
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
warn!("MySql pool error: {:?}", err);
CommonWebError::from_error(err)
})?;
// ==================== AVT TOKEN ==================== //
let maybe_avt_token = server_state.avt_cookie_manager.get_avt_token_from_request(&http_request);
// ==================== USER SESSION ==================== //
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::from_error(e)
})?;
if let Some(user_session) = maybe_user_session.as_ref() {
maybe_user_token = Some(UserToken::new_from_str(&user_session.user_token));
}
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment,
maybe_user_session.as_ref());
priority_level = plan.web_vc_base_priority_level();
// ==================== INVESTOR PRIORITY ==================== //
// TODO/TEMP: Give investors even more priority
let mut is_investor = false;
{
// TODO/TEMP: The storyteller.io website's AJAX calls will set this.
// This is just for the YCombinator demo.
match request.is_storyteller_demo {
Some(true) => {
is_investor = true;
},
_ => {},
};
// TODO/TEMP: The storyteller.io website will redirect and establish this cookie.
// This is just for the YCombinator demo.
if request_has_demo_cookie(&http_request) {
is_investor = true;
}
if is_investor {
priority_level = FAKEYOU_INVESTOR_PRIORITY_LEVEL;
}
}
// ==================== DEBUG MODE + ROUTING TAG ==================== //
let is_debug_request =
get_request_header_optional(&http_request, DEBUG_HEADER_NAME)
.is_some();
let maybe_routing_tag=
get_request_header_optional(&http_request, ROUTING_TAG_HEADER_NAME)
.map(|routing_tag| routing_tag.trim().to_string());
// ==================== BANNED USERS ==================== //
if let Some(ref user) = maybe_user_session {
if user.role.is_banned {
return Err(CommonWebError::NotAuthorized);
}
}
// ==================== RATE LIMIT ==================== //
if !disable_rate_limiter {
let mut rate_limiter = match maybe_user_session {
None => &server_state.redis_rate_limiters.logged_out,
Some(ref _session) => &server_state.redis_rate_limiters.logged_in
};
// TODO/TEMP
if is_investor {
rate_limiter = &server_state.redis_rate_limiters.logged_in;
}
if let Err(_err) = rate_limiter.rate_limit_request(&http_request).await {
return Err(CommonWebError::TooManyRequests);
}
}
// ==================== LOOK UP MODEL INFO ==================== //
// TODO(bt): CHECK DATABASE!
let model_token = request.voice_conversion_model_token.clone();
let model_inference_info = lookup_model_info(
&model_token,
&server_state.caches.durable.model_token_info,
&mut mysql_connection
).await?;
// ==================== HANDLE AUDIO INPUT ==================== //
let media_token = request.source_media_upload_token.clone();
let media_token_type =
if media_token.starts_with(MediaUploadToken::token_prefix()) {
InferenceInputSourceTokenType::MediaUpload
} else if media_token.starts_with(MediaFileToken::token_prefix()) {
InferenceInputSourceTokenType::MediaFile
} else {
return Err(CommonWebError::BadInputWithSimpleMessage(
"input token is not a media_upload or media_file token".to_string()));
};
// ==================== CHECK AND ENQUEUE VOICE CONVERSION ==================== //
let mut redis = server_state.redis_pool
.get()
.map_err(|e| {
error!("redis error: {:?}", e);
CommonWebError::from_error(e)
})?;
let redis_count_key = RedisKeys::web_vc_model_usage_count(&model_token);
redis.incr::<_, _, ()>(&redis_count_key, 1)
.map_err(|e| {
warn!("redis error: {:?}", e);
CommonWebError::from_error(e)
})?;
let ip_address = get_request_ip(&http_request);
let maybe_user_preferred_visibility : Option<Visibility> = maybe_user_session
.as_ref()
.map(|user_session| user_session.preferences.preferred_tts_result_visibility); // TODO: New setting for web-vc
let set_visibility = request.creator_set_visibility
.or(maybe_user_preferred_visibility)
.unwrap_or(Visibility::Public);
info!("Creating voice conversion inference job record...");
let mut maybe_args = None;
let set_args = request.auto_predict_f0.is_some()
|| request.transpose.is_some()
|| request.override_f0_method.is_some();
if set_args {
maybe_args = Some(PolymorphicInferenceArgs::Vc {
auto_predict_f0: request.auto_predict_f0,
override_f0_method: request.override_f0_method.map(|f| match f {
FundamentalFrequencyMethod::Crepe => FundamentalFrequencyMethodForJob::Crepe,
FundamentalFrequencyMethod::Dio => FundamentalFrequencyMethodForJob::Dio,
FundamentalFrequencyMethod::Harvest => FundamentalFrequencyMethodForJob::Harvest,
FundamentalFrequencyMethod::Rmvpe => FundamentalFrequencyMethodForJob::Rmvpe,
}),
transpose: request.transpose,
});
}
let job_type = match model_inference_info.job_model_type {
InferenceModelType::SoVitsSvc => InferenceJobType::SoVitsSvc,
InferenceModelType::RvcV2 => InferenceJobType::RvcV2,
_ => {
// In theory, this shouldn't catch anything.
error!("wrong model type for voice conversion: {:?}", model_inference_info.job_model_type);
return Err(CommonWebError::server_error_with_message("wrong model type for voice conversion"))
}
};
let maybe_product_category = match model_inference_info.job_model_type {
InferenceModelType::RvcV2 => Some(InferenceJobProductCategory::VcRvc2),
InferenceModelType::SoVitsSvc => Some(InferenceJobProductCategory::VcSvc),
_ => None,
};
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
job_type,
maybe_product_category,
inference_category: InferenceCategory::VoiceConversion,
maybe_model_type: Some(model_inference_info.job_model_type),
maybe_model_token: Some(&model_token),
maybe_input_source_token: Some(&media_token),
maybe_input_source_token_type: Some(media_token_type),
maybe_download_url: None,
maybe_cover_image_media_file_token: None,
maybe_raw_inference_text: None, // NB: Voice conversion isn't TTS, so there's no text.
maybe_max_duration_seconds: None,
maybe_inference_args: Some(GenericInferenceArgs {
inference_category: Some(InferenceCategoryAbbreviated::VoiceConversion),
args: maybe_args,
}),
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: set_visibility,
priority_level,
requires_keepalive: plan.web_vc_requires_frontend_keepalive(),
is_debug_request,
maybe_routing_tag: maybe_routing_tag.as_deref(),
mysql_pool: &server_state.mysql_pool,
}).await;
let job_token = match query_result {
Ok((job_token, _id)) => job_token,
Err(err) => {
warn!("New generic inference job creation DB error: {:?}", err);
if err.had_duplicate_idempotency_token() {
return Err(CommonWebError::BadInputWithSimpleMessage("Duplicate idempotency token".to_string()));
}
return Err(CommonWebError::from_error(err));
}
};
server_state.firehose_publisher.enqueue_vc_inference(
maybe_user_token.as_ref(),
&job_token)
.await
.map_err(|e| {
warn!("error publishing event: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
Ok(Json(EnqueueVoiceConversionInferenceSuccessResponse {
success: true,
inference_job_token: job_token,
}))
}
async fn lookup_model_info(
model_token: &str,
cache: &ModelTokenToInfoCache,
mysql_connection: &mut PoolConnection<MySql>,
) -> Result<ModelInfoForInferenceJob, CommonWebError> {
let maybe_model_inference_info = cache
.get_info(model_token)
.map_err(|err| {
error!("in-memory cache error: {:?}", err);
CommonWebError::from_anyhow_error(err)
})?;
if let Some(inference_info) = maybe_model_inference_info {
return Ok(inference_info);
}
let result = query_vc_model_info_lite_for_migration_with_connection(
model_token, mysql_connection)
.await
.map_err(|err| {
error!("model lookup error: {:?}", err);
CommonWebError::from_anyhow_error(err)
})?;
let model_info = match result {
Some(model_info) => model_info,
None => {
warn!("model not found for {model_token}; failing open");
// NB: Fail open for now.
return Ok(ModelInfoForInferenceJob {
job_inference_category: InferenceCategory::VoiceConversion,
job_model_type: InferenceModelType::SoVitsSvc,
});
}
};
let inference_info = match (model_info.get_inference_category(), model_info.get_inference_model_type()) {
(Some(inference_category), Some(model_type)) => {
ModelInfoForInferenceJob {
job_inference_category: inference_category,
job_model_type: model_type,
}
}
_ => {
error!("wrong model type for inference: {:?}", model_info);
return Err(CommonWebError::BadInputWithSimpleMessage("wrong model type for inference".to_string()));
}
};
cache.insert_one(model_token, &inference_info)
.map_err(|err| {
error!("cache insertion error: {:?}", err);
CommonWebError::from_anyhow_error(err)
})?;
Ok(inference_info)
}
@@ -1,218 +0,0 @@
// NB: Incrementally getting rid of build warnings...
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
use std::sync::Arc;
use actix_web::{web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};
use lexical_sort::natural_lexical_cmp;
use log::{debug, error, warn};
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use crate::http_server::common_responses::user_details_lite::UserDetailsLight;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use enums::by_table::voice_conversion_models::voice_conversion_model_type::VoiceConversionModelType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use migration::voice_conversion::list_vc_models_for_migration::list_vc_models_for_migration;
use mysql_queries::queries::users::user_sessions::get_user_session_by_token::SessionUserRecord;
use crate::state::server_state::ServerState;
#[derive(Serialize, Clone)]
pub struct VoiceConversionModel {
/// NB: token can be a `model_weights` token or a `voice_conversion_models` token.
pub token: String,
pub model_type: VoiceConversionModelType,
pub title: String,
pub creator: UserDetailsLight,
pub creator_set_visibility: Visibility,
pub ietf_language_tag: String,
pub ietf_primary_language_subtag: String,
pub is_front_page_featured: bool,
// TODO: Add user ratings.
//pub user_ratings: UserRatingsStats,
// TODO: Add categories.
///// Category assignments
///// From non-deleted, mod-approved categories only
//pub category_tokens: HashSet<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
// TODO: Add user ratings.
//#[derive(Serialize, Clone)]
//pub struct UserRatingsStats {
// pub positive_count: u32,
// pub negative_count: u32,
// /// Total count does not take into account "neutral" ratings.
// pub total_count: u32,
//}
#[derive(Serialize)]
pub struct ListVoiceConversionModelsSuccessResponse {
pub success: bool,
pub models: Vec<VoiceConversionModel>,
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
pub async fn list_voice_conversion_models_handler(
http_request: HttpRequest,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, CommonWebError>
{
if server_state.flags.disable_voice_conversion_model_list_endpoint {
// NB: Despite the cache being a powerful protector of the database (this is an expensive query),
// if the cache goes stale during an outage, there is no protection. This feature flag lets us
// shut off all traffic to the endpoint.
return render_response_busy(ListVoiceConversionModelsSuccessResponse {
success: true,
models: Vec::new(),
});
}
let maybe_models = server_state.caches.ephemeral.voice_conversion_model_list.grab_copy_without_bump_if_unexpired()
.map_err(|e| {
error!("Error consulting cache: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
// NB: We don't know if we need a MySQL connection, so don't grab one unless we do.
let mut maybe_mysql_connection = None;
let models = match maybe_models {
Some(models) => {
debug!("Serving voice conversion models from cache");
models
},
None => {
debug!("Populating voice conversion models from database");
let mut mysql_connection = server_state.mysql_pool.acquire()
.await
.map_err(|e| {
warn!("Could not acquire DB pool: {:?}", e);
CommonWebError::from_error(e)
})?;
// TODO: Fail open in case the DB is down. Pull from expired cache if query fails.
let models = get_all_models(&mut mysql_connection, true)
.await
.map_err(|e| {
error!("Error querying database: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
maybe_mysql_connection = Some(mysql_connection);
server_state.caches.ephemeral.voice_conversion_model_list.store_copy(&models)
.map_err(|e| {
error!("Error storing cache: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
models
},
};
let maybe_user_session : Option<SessionUserRecord> = match maybe_mysql_connection {
None => {
server_state.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
}
Some(mut mysql_connection) => {
server_state.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
}
}.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::from_error(e)
})?;
let maybe_session_user_token = maybe_user_session
.as_ref()
.map(|s| s.user_token.as_str());
let models = models.into_iter()
.filter(|model| {
match model.creator_set_visibility {
Visibility::Public => true,
Visibility::Hidden | Visibility::Private => maybe_session_user_token
.map(|token| token == model.creator.user_token.as_str())
.unwrap_or(false),
}
})
.collect();
render_response_ok(ListVoiceConversionModelsSuccessResponse {
success: true,
models,
})
}
pub fn render_response_busy(response: ListVoiceConversionModelsSuccessResponse) -> Result<HttpResponse, CommonWebError> {
let body = render_response_payload(response)?;
Ok(HttpResponse::TooManyRequests()
.content_type("application/json")
.body(body))
}
pub fn render_response_ok(response: ListVoiceConversionModelsSuccessResponse) -> Result<HttpResponse, CommonWebError> {
let body = render_response_payload(response)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}
pub fn render_response_payload(response: ListVoiceConversionModelsSuccessResponse) -> Result<String, CommonWebError> {
let body = serde_json::to_string(&response)
.map_err(|e| {
error!("error returning response: {:?}", e);
CommonWebError::from_error(e)
})?;
Ok(body)
}
async fn get_all_models(mysql_connection: &mut PoolConnection<MySql>, use_weights_table: bool) -> AnyhowResult<Vec<VoiceConversionModel>> {
let mut models = list_vc_models_for_migration(mysql_connection, use_weights_table).await?;
// NB: Make the list nice for human readers.
models.sort_by(|a, b|
natural_lexical_cmp(&a.title(), &b.title()));
let models_for_response = models.into_iter()
.filter(|model| model.is_voice_conversion_model())
.map(|model| {
// TODO(bt,2023-12-18): All of these clones are lame, but this is just to support the migration.
VoiceConversionModel {
token: model.token().to_string(),
model_type: model.legacy_voice_conversion_model_type()
.unwrap_or(VoiceConversionModelType::SoVitsSvc),
title: model.title().to_string(),
is_front_page_featured: false, // TODO(bt,2023-12-18): None of the models are "featured" yet.
creator: UserDetailsLight::from_db_fields(
model.creator_user_token(),
model.creator_username(),
model.creator_display_name(),
model.creator_gravatar_hash(),
),
creator_set_visibility: model.creator_set_visibility(),
ietf_language_tag: model.ietf_language_tag().to_string(),
ietf_primary_language_subtag: model.ietf_primary_language_subtag().to_string(),
created_at: *model.created_at(),
updated_at: *model.updated_at(),
}
})
.collect::<Vec<VoiceConversionModel>>();
Ok(models_for_response)
}
@@ -1,3 +0,0 @@
pub mod enqueue_voice_conversion_inference_handler;
pub mod list_voice_conversion_models_handler;
pub mod enqueue_seed_vc_inference_handler;
@@ -1,199 +0,0 @@
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
use std::sync::Arc;
use actix_web::{web, HttpRequest};
use actix_web::web::Json;
use log::warn;
use serde::Deserialize;
use serde::Serialize;
use utoipa::ToSchema;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use enums::common::visibility::Visibility;
use http_server_common::request::get_request_header_optional::get_request_header_optional;
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{
GenericInferenceArgs,
InferenceCategoryAbbreviated,
PolymorphicInferenceArgs,
};
use mysql_queries::payloads::generic_inference_args::inner_payloads::tts_payload::TTSArgs;
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{
insert_generic_inference_job,
InsertGenericInferenceArgs,
};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::users::UserToken;
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::state::server_state::ServerState;
/// Debug requests can get routed to special "debug-only" workers, which can
/// be used to trial new code, run debugging, etc.
const DEBUG_HEADER_NAME: &str = "enable-debug-mode";
/// The routing tag header can send workloads to particular k8s hosts.
/// This is useful for catching the live logs or intercepting the job.
const ROUTING_TAG_HEADER_NAME: &str = "routing-tag";
#[derive(Deserialize,ToSchema)]
pub struct EnqueueTTSRequest {
uuid_idempotency_token: String,
text: String,
voice_token: String,
}
#[derive(Serialize,ToSchema)]
pub struct EnqueueTTSRequestSuccessResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
// Implementation for enqueuing a TTS request
// Reference enqueue_infer_tts_handler.rs for checks: rate limiting / user sessions
// insert generic inference job.rs
// Need to convert it to generic inference job.
#[utoipa::path(
post,
tag = "Voice Designer",
path = "/v1/voice_designer/inference/enqueue_tts",
responses(
(status = 200, description = "Enqueue TTS generically", body = EnqueueTTSRequestSuccessResponse),
(status = 400, description = "Bad input", body = CommonWebError),
(status = 401, description = "Not authorized", body = CommonWebError),
(status = 429, description = "Rate limited", body = CommonWebError),
(status = 500, description = "Server error", body = CommonWebError)
),
params(
("request" = EnqueueTTSRequest, description = "Payload for TTS Request")
)
)]
pub async fn enqueue_tts_request(
http_request: HttpRequest,
request: Json<EnqueueTTSRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<EnqueueTTSRequestSuccessResponse>, CommonWebError> {
let mut maybe_user_token: Option<UserToken> = None;
let mut mysql_connection = server_state.mysql_pool.acquire().await.map_err(|err| {
warn!("MySql pool error: {:?}", err);
CommonWebError::from_error(err)
})?;
// ==================== USER SESSION ==================== //
let maybe_user_session = server_state.session_checker
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection).await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::from_error(e)
})?;
if let Some(user_session) = maybe_user_session.as_ref() {
maybe_user_token = Some(UserToken::new_from_str(&user_session.user_token));
}
// Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment,
maybe_user_session.as_ref()
);
// Separate priority for animation.
let priority_level = plan.web_vc_base_priority_level();
// ==================== DEBUG MODE + ROUTING TAG ==================== //
let is_debug_request = get_request_header_optional(&http_request, DEBUG_HEADER_NAME).is_some();
let maybe_routing_tag = get_request_header_optional(&http_request, ROUTING_TAG_HEADER_NAME).map(
|routing_tag| routing_tag.trim().to_string()
);
// ==================== BANNED USERS ==================== //
if let Some(ref user) = maybe_user_session {
if user.role.is_banned {
return Err(CommonWebError::NotAuthorized);
}
}
// ==================== RATE LIMIT ==================== //
let rate_limiter = match maybe_user_session {
None => &server_state.redis_rate_limiters.logged_out,
Some(ref _user) => &server_state.redis_rate_limiters.logged_in,
};
if let Err(_err) = rate_limiter.rate_limit_request(&http_request).await {
return Err(CommonWebError::TooManyRequests);
}
// Get up IP address
let ip_address = get_request_ip(&http_request);
// package as larger component args should always have an embedding token ..
let inference_args = TTSArgs {
voice_token: Some(request.voice_token.clone()),
dataset_token: None,
};
// create the inference args here
// enqueue a zero shot tts request here...
let maybe_avt_token = server_state.avt_cookie_manager.get_avt_token_from_request(&http_request);
// create the job record here!
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
job_type: InferenceJobType::StyleTTS2,
maybe_product_category: Some(InferenceJobProductCategory::TtsStyleTts2),
inference_category: InferenceCategory::TextToSpeech,
maybe_model_type: Some(InferenceModelType::StyleTTS2), // NB: Model is static during inference
maybe_model_token: None, // NB: Model is static during inference
maybe_input_source_token: None,
maybe_input_source_token_type: None,
maybe_download_url: None,
maybe_cover_image_media_file_token: None,
maybe_raw_inference_text: Some(&request.text),
maybe_max_duration_seconds: None,
maybe_inference_args: Some(GenericInferenceArgs {
inference_category: Some(InferenceCategoryAbbreviated::TextToSpeech),
args: Some(PolymorphicInferenceArgs::Tts(inference_args)),
}),
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
priority_level,
requires_keepalive: true, // do we need this? I think so
is_debug_request,
maybe_routing_tag: maybe_routing_tag.as_deref(),
mysql_pool: &server_state.mysql_pool,
}).await;
let job_token = match query_result {
Ok((job_token, _id)) => job_token,
Err(err) => {
warn!("New generic inference job creation DB error: {:?}", err);
if err.had_duplicate_idempotency_token() {
return Err(CommonWebError::BadInputWithSimpleMessage("Duplicate idempotency token".to_string()));
}
return Err(CommonWebError::from_error(err));
}
};
let response: EnqueueTTSRequestSuccessResponse = EnqueueTTSRequestSuccessResponse {
success: true,
inference_job_token: job_token,
};
Ok(Json(response))
}
@@ -1,6 +0,0 @@
use actix_web::HttpResponse;
pub async fn enqueue_vc_request() -> HttpResponse {
// Implementation for enqueuing a VC request
HttpResponse::Ok().json("VC request enqueued successfully")
}
@@ -1,2 +0,0 @@
pub mod enqueue_tts_request;
pub mod enqueue_vc_request;
@@ -1,4 +0,0 @@
pub mod inference;
pub mod voice_dataset_samples;
pub mod voice_datasets;
pub mod voices;
@@ -1,153 +0,0 @@
{
"info": {
"name": "Voice Designer API",
"description": "Endpoints for the Voice Designer API",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Voice Designer - Datasets",
"item": [
{
"name": "Create Dataset",
"request": {
"method": "POST",
"url": "{{base_url}}/v1/voice_designer/datasets/create"
}
},
{
"name": "Update Dataset",
"request": {
"method": "POST",
"url": "{{base_url}}/v1/voice_designer/datasets/{{dataset_token}}/update"
}
},
{
"name": "Delete Dataset",
"request": {
"method": "DELETE",
"url": "{{base_url}}/v1/voice_designer/datasets/{{dataset_token}}/delete"
}
},
{
"name": "List Datasets By User",
"request": {
"method": "GET",
"url": "{{base_url}}/v1/voice_designer/datasets/user/{{user_token}}/list"
}
}
]
},
{
"name": "Voice Designer - Voice",
"item": [
{
"name": "Create Voice",
"request": {
"method": "POST",
"url": "{{base_url}}/v1/voice_designer/voice/create"
}
},
{
"name": "Update Voice",
"request": {
"method": "POST",
"url": "{{base_url}}/v1/voice_designer/voice/{{voice_token}}/update"
}
},
{
"name": "Delete Voice",
"request": {
"method": "DELETE",
"url": "{{base_url}}/v1/voice_designer/voice/{{voice_token}}/delete"
}
},
{
"name": "List Voices By User",
"request": {
"method": "GET",
"url": "{{base_url}}/v1/voice_designer/voice/user/{{user_token}}/list"
}
}
]
},
{
"name": "Voice Designer - Sample",
"item": [
{
"name": "Upload Sample",
"request": {
"method": "POST",
"url": "{{base_url}}/v1/voice_designer/sample/upload"
}
},
{
"name": "Delete Sample",
"request": {
"method": "DELETE",
"url": "{{base_url}}/v1/voice_designer/sample/delete"
}
},
{
"name": "List Samples By Dataset",
"request": {
"method": "GET",
"url": "{{base_url}}/v1/voice_designer/sample/data_set/{{data_set_token}}/list"
}
}
]
},
{
"name": "Voice Designer - Inference",
"item": [
{
"name": "Enqueue TTS Request",
"request": {
"method": "POST",
"url": "{{base_url}}/v1/voice_designer/inference/enqueue_tts"
}
},
{
"name": "Enqueue VC Request",
"request": {
"method": "POST",
"url": "{{base_url}}/v1/voice_designer/inference/enqueue_vc"
}
}
]
},
{
"name": "Voice Designer - Inventory",
"item": [
{
"name": "List Available Voices",
"request": {
"method": "GET",
"url": "{{base_url}}/v1/voice_designer/inventory/list"
}
},
{
"name": "List User Models",
"request": {
"method": "GET",
"url": "{{base_url}}/v1/voice_designer/inventory/user_list"
}
},
{
"name": "List Favorite Models",
"request": {
"method": "GET",
"url": "{{base_url}}/v1/voice_designer/inventory/favorites_list"
}
},
{
"name": "Search Voices",
"request": {
"method": "GET",
"url": "{{base_url}}/v1/voice_designer/inventory/search"
}
}
]
}
]
}
@@ -1,125 +0,0 @@
use std::sync::Arc;
use actix_web::web::Path;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use log::warn;
use artcraft_api_defs::common::responses::simple_generic_json_success::SimpleGenericJsonSuccess;
use mysql_queries::queries::voice_designer::voice_samples::delete_sample::{delete_sample_as_mod, delete_sample_as_user, undelete_sample_as_mod, undelete_sample_as_user};
use mysql_queries::queries::voice_designer::voice_samples::get_dataset_sample::get_dataset_sample_by_token;
use tokens::tokens::zs_voice_dataset_samples::ZsVoiceDatasetSampleToken;
use crate::state::server_state::ServerState;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
// TODO(bt,2023-10-10): This is way too much boilerplate.
/// For the URL PathInfo
#[derive(Deserialize)]
pub struct DeleteSamplePathInfo {
sample_token: ZsVoiceDatasetSampleToken,
}
#[derive(Deserialize)]
pub struct DeleteSampleRequest {
set_delete: bool,
/// NB: this is only to disambiguate when a user is both a mod and an author.
as_mod: Option<bool>,
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
pub async fn delete_sample_handler(
http_request: HttpRequest,
path: Path<DeleteSamplePathInfo>,
request: Json<DeleteSampleRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<SimpleGenericJsonSuccess>, CommonWebError> {
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::from_error(e)
})?;
let user_session = match maybe_user_session {
Some(session) => session,
None => {
warn!("not logged in");
return Err(CommonWebError::NotAuthorized);
}
};
// NB: First permission check.
// Only mods should see deleted models (both user_* and mod_* deleted).
let is_mod_that_can_see_deleted = user_session.can_delete_other_users_tts_results;
let inference_result_query_result = get_dataset_sample_by_token(
&path.sample_token,
is_mod_that_can_see_deleted,
&server_state.mysql_pool,
).await;
let dataset_sample = match inference_result_query_result {
Err(e) => {
warn!("query error: {:?}", e);
return Err(CommonWebError::from_anyhow_error(e));
}
Ok(None) => return Err(CommonWebError::NotFound),
Ok(Some(sample)) => sample,
};
// NB: Second set of permission checks
let is_author = dataset_sample.maybe_creator_user_token
.as_ref()
.map(|creator_token| creator_token == &user_session.user_token)
.unwrap_or(false);
let is_mod = user_session.can_delete_other_users_tts_results;
if !is_author && !is_mod {
warn!("user is not allowed to delete samples: {:?}", user_session.user_token);
return Err(CommonWebError::NotAuthorized);
}
let delete_role = delete_role_disambiguation(is_mod, is_author, request.as_mod);
let query_result = if request.set_delete {
match delete_role {
DeleteRole::ErrorDoNotDelete => {
return Err(CommonWebError::NotAuthorized);
}
DeleteRole::AsUser => {
delete_sample_as_user(&path.sample_token, &server_state.mysql_pool).await
}
DeleteRole::AsMod => {
delete_sample_as_mod(&path.sample_token, &user_session.user_token, &server_state.mysql_pool).await
}
}
} else {
match delete_role {
DeleteRole::ErrorDoNotDelete => {
return Err(CommonWebError::NotAuthorized);
}
DeleteRole::AsUser => {
undelete_sample_as_user(&path.sample_token, &server_state.mysql_pool).await
}
DeleteRole::AsMod => {
undelete_sample_as_mod(&path.sample_token, &user_session.user_token, &server_state.mysql_pool).await
}
}
};
match query_result {
Ok(_) => {},
Err(err) => {
warn!("Delete DB error: {:?}", err);
return Err(CommonWebError::from_anyhow_error(err));
}
};
Ok(Json(SimpleGenericJsonSuccess { success: true }))
}
@@ -1,111 +0,0 @@
use std::sync::Arc;
use actix_web::{web, HttpRequest};
use actix_web::web::Json;
use chrono::{DateTime, Utc};
use log::warn;
use enums::by_table::media_files::media_file_type::MediaFileType;
use mysql_queries::queries::voice_designer::voice_samples::list_dataset_samples_for_dataset_token::list_dataset_samples_for_dataset_token;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use tokens::tokens::zs_voice_dataset_samples::ZsVoiceDatasetSampleToken;
use tokens::tokens::zs_voice_datasets::ZsVoiceDatasetToken;
use crate::state::server_state::ServerState;
use crate::http_server::common_responses::common_web_error::CommonWebError;
#[derive(Serialize, Clone)]
pub struct ZsSampleRecordForResponse {
sample_token: ZsVoiceDatasetSampleToken,
media_file_token: MediaFileToken,
media_type: MediaFileType,
public_bucket_directory_hash: String,
maybe_public_bucket_prefix: Option<String>,
maybe_public_bucket_extension: Option<String>,
maybe_creator_user_token: Option<UserToken>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
#[derive(Serialize)]
pub struct ListSamplesByDatasetSuccessResponse {
pub success: bool,
pub samples: Vec<ZsSampleRecordForResponse>,
}
#[derive(Deserialize)]
pub struct ListSamplesByDatasetPathInfo {
dataset_token: String,
}
pub async fn list_samples_by_dataset_handler(
http_request: HttpRequest,
path: web::Path<ListSamplesByDatasetPathInfo>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<ListSamplesByDatasetSuccessResponse>, CommonWebError> {
let maybe_user_session = server_state.session_checker.maybe_get_user_session(
&http_request,
&server_state.mysql_pool
).await.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::from_error(e)
})?;
let user_session = match maybe_user_session {
Some(session) => session,
None => {
warn!("not logged in");
return Err(CommonWebError::NotAuthorized);
},
};
let dataset_token = ZsVoiceDatasetToken(path.dataset_token.clone());
let _is_mod = user_session.can_ban_users;
// NB(bt,2024-01-18): Showing mods deleted files is actually kind of annoying!
// We should focus on visibility-related controls instead.
const CAN_SEE_DELETED : bool = false;
let query_results = list_dataset_samples_for_dataset_token(
&dataset_token,
CAN_SEE_DELETED,
&server_state.mysql_pool
).await.map_err(|e| {
warn!("list_samples error: {:?}", e);
CommonWebError::from_anyhow_error(e)
});
let samples = match query_results {
Ok(samples) => samples,
Err(e) => {
warn!("list_samples error: {:?}", e);
return Err(CommonWebError::from_error(e));
},
};
let samples = samples
.into_iter()
.map(|sample| ZsSampleRecordForResponse {
sample_token: sample.sample_token,
media_file_token: sample.media_file_token,
media_type: sample.media_type,
public_bucket_directory_hash: sample.public_bucket_directory_hash,
maybe_public_bucket_prefix: sample.maybe_public_bucket_prefix,
maybe_public_bucket_extension: sample.maybe_public_bucket_extension,
maybe_creator_user_token: sample.maybe_creator_user_token,
created_at: sample.created_at,
updated_at: sample.updated_at,
})
.collect();
let response = ListSamplesByDatasetSuccessResponse {
success: true,
samples
};
Ok(Json(response))
}
@@ -1,3 +0,0 @@
pub mod delete_sample;
pub mod upload_zs_sample;
pub mod list_samples_by_dataset;

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