mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-22 01:29:31 -04:00
Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a58a60475 | ||
|
|
55fab85170 | ||
|
|
9cf1b8e066 | ||
|
|
aebc6ab777 | ||
|
|
d3c89face5 | ||
|
|
692e0ecf75 | ||
|
|
94e1aef78d | ||
|
|
9a4bb3b490 | ||
|
|
299d9b8d23 | ||
|
|
89eff46c6f | ||
|
|
cec3b50ae4 | ||
|
|
3040aaa0d1 | ||
|
|
f47e8b9b14 | ||
|
|
83115e7ef9 | ||
|
|
4fbe9f54e0 | ||
|
|
e850022fe8 | ||
|
|
1a528bba45 | ||
|
|
b625d5ff79 | ||
|
|
d8fd0bfe64 | ||
|
|
3cc02ae7bf | ||
|
|
2c943b61ab | ||
|
|
d92e6fc75c | ||
|
|
c0ca274168 | ||
|
|
27cc125089 | ||
|
|
4ce3f240c7 | ||
|
|
3a2e3b30ec | ||
|
|
82c0a7a4d9 | ||
|
|
cbd3710f4f | ||
|
|
044c6d8a88 | ||
|
|
353c49c55f | ||
|
|
98ea5cdb62 | ||
|
|
8fdc5100af | ||
|
|
b782cb0c67 | ||
|
|
f01e81b66a | ||
|
|
3c0d1415e5 | ||
|
|
5e4b7f271d | ||
|
|
2371dfd49e | ||
|
|
2f20e7b3a3 |
+12
@@ -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"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE tts_results\nSET\n mod_deleted_at = CURRENT_TIMESTAMP,\n maybe_mod_user_token = ?\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "376e36b1357952599866262c7356ba35f15ccdfb338d8ccdbdc06ccd69331fc3"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE tts_results\nSET\n mod_deleted_at = NULL,\n maybe_mod_user_token = ?\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "40a3301e9a3201e74d49b6b339f67291e2ae22d315188401708609ac2d28a68c"
|
||||
}
|
||||
-12
@@ -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"
|
||||
}
|
||||
-36
@@ -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"
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n job_type as queue_type,\n count(*) as pending_job_count,\n NOW() as present_time\n FROM (\n SELECT\n token,\n job_type\n FROM generic_inference_jobs\n WHERE status IN (\"pending\", \"attempt_failed\")\n AND job_type IS NOT NULL\n UNION\n SELECT\n token,\n job_type\n FROM generic_inference_jobs\n WHERE status IN (\"started\")\n AND created_at > (CURDATE() - INTERVAL 15 MINUTE)\n AND job_type IS NOT NULL\n) as generic_inner\nGROUP BY queue_type\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "queue_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "pending_job_count",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "present_time",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "73dc054488b2fa3efc923d3a90914a1450b73e99a377562484a0498ccce1676f"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO tts_model_upload_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n creator_user_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n title = ?,\n tts_model_type = ?,\n download_url = ?,\n status = \"pending\"\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 8
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "886861e91c7e8af53eb856c949389a172b8d9e079f1c2070d115423907c47c50"
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n users.token as `user_token: UserToken`,\n users.username,\n users.display_name,\n users.email_address,\n users.user_role_slug,\n users.is_banned,\n user_roles.can_ban_users,\n api_keys.token as `api_key_token: ApiKeyToken`\nFROM api_keys\nINNER JOIN users\n ON users.token = api_keys.owner_user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE api_keys.api_key = ?\n AND api_keys.maybe_deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "email_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "user_role_slug",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_banned",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "can_ban_users",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "api_key_token: ApiKeyToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9188e235caa439d50628333c2f4722b9f1a8f575c192d4d36a2039709c412ac0"
|
||||
}
|
||||
-256
@@ -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"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE tts_results\nSET\n user_deleted_at = CURRENT_TIMESTAMP\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "961f792c24bee6009e3e8a215561db932871a53c48839b8cb80292de00e7b2e1"
|
||||
}
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as job_token,\n\n jobs.status,\n jobs.attempt_count,\n jobs.on_success_result_token as maybe_result_token,\n results.public_bucket_wav_audio_path as maybe_public_bucket_wav_audio_path,\n\n jobs.model_token,\n tts.tts_model_type,\n tts.title,\n\n jobs.raw_inference_text,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM tts_inference_jobs as jobs\nJOIN tts_models as tts\n ON tts.token = jobs.model_token\nLEFT OUTER JOIN tts_results as results\n ON jobs.on_success_result_token = results.token\n\nWHERE jobs.token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "job_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "status",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "attempt_count",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_result_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_public_bucket_wav_audio_path",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "model_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "tts_model_type",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
|
||||
"char_set": 224,
|
||||
"max_size": 60
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "raw_inference_text",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 262140
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a2b586dd2120f29dd26281626f7381e292c821f8e826b9bc584b92ffa45035b2"
|
||||
}
|
||||
-80
@@ -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"
|
||||
}
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n users.username,\n users.display_name,\n users.email_gravatar_hash as gravatar_hash,\n creator_user_token,\n count(*) as uploaded_count\nFROM tts_models\nJOIN users\nON\n users.token = creator_user_token\nWHERE\n users.is_banned IS FALSE\n AND users.token NOT IN (\"U:DRB1SKEMKHT1J\")\n AND tts_models.user_deleted_at IS NULL\n AND tts_models.mod_deleted_at IS NULL\nGROUP BY creator_user_token\nORDER BY uploaded_count desc\nLIMIT 25;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "gravatar_hash",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "creator_user_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "uploaded_count",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 21
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a82fa4eb36528ebf52af2585cdda203476a4ba3645837c792830474fca2eda4f"
|
||||
}
|
||||
-12
@@ -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"
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n e.maybe_user_token AS `user_token!: UserToken`,\n u.username AS `username!`,\n u.display_name AS `display_name!`,\n u.email_gravatar_hash AS `email_gravatar_hash!`,\n CAST(SUM(CASE WHEN e.amount_usd_cents > 0 THEN e.amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `gross_spend_usd_cents!: u64`,\n CAST(SUM(CASE WHEN e.amount_usd_cents < 0 THEN -e.amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `refund_usd_cents!: u64`,\n CAST(SUM(e.amount_usd_cents) AS SIGNED) AS `net_spend_usd_cents!: i64`,\n CAST(SUM(CASE WHEN e.amount_usd_cents > 0 THEN 1 ELSE 0 END) AS UNSIGNED) AS `payment_count!: u64`,\n CAST(COALESCE(SUM(e.maybe_credits_granted), 0) AS UNSIGNED) AS `credits_granted!: u64`\nFROM user_spend_events e\nJOIN users u ON u.token = e.maybe_user_token\nWHERE e.maybe_user_token IS NOT NULL\n AND e.is_production = TRUE\n AND e.payment_occurred_at >= ?\n AND (? IS NULL OR e.payments_namespace = ?)\nGROUP BY e.maybe_user_token, u.username, u.display_name, u.email_gravatar_hash\nORDER BY SUM(e.amount_usd_cents) DESC, e.maybe_user_token ASC\nLIMIT ? OFFSET ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "user_token!: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "username!",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "display_name!",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "email_gravatar_hash!",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "gross_spend_usd_cents!: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "UNSIGNED | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "refund_usd_cents!: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "UNSIGNED | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "net_spend_usd_cents!: i64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "payment_count!: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "UNSIGNED | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "credits_granted!: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | UNSIGNED | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 21
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c2ee41eb6bc1a6a551411998155423142411010f298f0815eacc7d9c3138bdd8"
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n users.token as `user_token: UserToken`,\n users.username,\n users.display_name,\n users.email_address,\n users.user_role_slug,\n users.is_banned,\n user_roles.can_ban_users\nFROM user_sessions\nINNER JOIN users\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "email_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "user_role_slug",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_banned",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "can_ban_users",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c77126adb36f9e2f0248720eb4d8c1cd9d14719f64a02678d8021e26addd2c18"
|
||||
}
|
||||
+146
@@ -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"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE tts_results\nSET\n user_deleted_at = NULL\nWHERE\n token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "dbce51eb1c80dded902c9509e50713483c7131f115e8ec10646e99e88110278b"
|
||||
}
|
||||
-256
@@ -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"
|
||||
}
|
||||
+146
@@ -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
+16
@@ -765,6 +765,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"strum",
|
||||
"test_data",
|
||||
"test_utils",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"url_utils",
|
||||
@@ -3621,6 +3622,7 @@ dependencies = [
|
||||
"env_logger 0.11.9",
|
||||
"log",
|
||||
"storyteller_root",
|
||||
"trace_id",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -6766,6 +6768,7 @@ version = "0.0.1"
|
||||
dependencies = [
|
||||
"env_logger 0.11.9",
|
||||
"log",
|
||||
"trace_id",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -8286,6 +8289,7 @@ dependencies = [
|
||||
"rootly_client",
|
||||
"sha2 0.11.0-rc.5",
|
||||
"tokio",
|
||||
"trace_id",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -11678,6 +11682,7 @@ dependencies = [
|
||||
"tokens",
|
||||
"tokio",
|
||||
"toml 1.0.7+spec-1.1.0",
|
||||
"trace_id",
|
||||
"tts_common",
|
||||
"url",
|
||||
"url_config",
|
||||
@@ -13316,6 +13321,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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
+6
@@ -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;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE debug_logs
|
||||
DROP INDEX index_maybe_log_level;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE debug_logs
|
||||
ADD INDEX index_maybe_log_level (maybe_log_level);
|
||||
+6
@@ -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;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files
|
||||
DROP INDEX idx_creator_created_at,
|
||||
ALGORITHM=INPLACE, LOCK=NONE;
|
||||
+17
@@ -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;
|
||||
|
||||
+36
@@ -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;
|
||||
+76
@@ -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>,
|
||||
}
|
||||
+30
@@ -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>,
|
||||
}
|
||||
+37
@@ -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,6 @@
|
||||
pub mod alerts;
|
||||
pub mod debug_logs;
|
||||
pub mod top_spenders;
|
||||
pub mod jobs;
|
||||
pub mod user;
|
||||
pub mod user_referrals;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::users::UserToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
pub const MODERATOR_LIST_TOP_SPENDERS_PATH: &str =
|
||||
"/v1/moderation/top_spenders/list";
|
||||
|
||||
/// Rolling aggregation window for the top spenders list.
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, ToSchema)]
|
||||
pub enum TopSpendersWindow {
|
||||
#[serde(rename = "24h")]
|
||||
TwentyFourHours,
|
||||
#[serde(rename = "7d")]
|
||||
SevenDays,
|
||||
#[serde(rename = "14d")]
|
||||
FourteenDays,
|
||||
#[serde(rename = "30d")]
|
||||
ThirtyDays,
|
||||
#[serde(rename = "60d")]
|
||||
SixtyDays,
|
||||
#[serde(rename = "90d")]
|
||||
NinetyDays,
|
||||
#[serde(rename = "180d")]
|
||||
OneHundredEightyDays,
|
||||
#[serde(rename = "365d")]
|
||||
ThreeHundredSixtyFiveDays,
|
||||
}
|
||||
|
||||
impl TopSpendersWindow {
|
||||
/// Window length in hours.
|
||||
pub fn as_hours(self) -> i64 {
|
||||
match self {
|
||||
TopSpendersWindow::TwentyFourHours => 24,
|
||||
TopSpendersWindow::SevenDays => 7 * 24,
|
||||
TopSpendersWindow::FourteenDays => 14 * 24,
|
||||
TopSpendersWindow::ThirtyDays => 30 * 24,
|
||||
TopSpendersWindow::SixtyDays => 60 * 24,
|
||||
TopSpendersWindow::NinetyDays => 90 * 24,
|
||||
TopSpendersWindow::OneHundredEightyDays => 180 * 24,
|
||||
TopSpendersWindow::ThreeHundredSixtyFiveDays => 365 * 24,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub struct ModeratorListTopSpendersQueryParams {
|
||||
/// Aggregation window: `24h`, `7d`, `14d`, `30d` (default), `60d`, `90d`,
|
||||
/// `180d`, or `365d`.
|
||||
pub window: Option<TopSpendersWindow>,
|
||||
/// Row offset for pagination (page size is fixed at 100). Pass `next_offset` from the previous page.
|
||||
pub offset: Option<u64>,
|
||||
/// Optional namespace filter (e.g. `artcraft`). Omit to return all namespaces.
|
||||
pub payments_namespace: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModeratorListTopSpendersResponse {
|
||||
pub success: bool,
|
||||
/// The window the results were aggregated over (echoed back, post-default).
|
||||
pub window: TopSpendersWindow,
|
||||
pub spenders: Vec<TopSpenderEntry>,
|
||||
/// Offset for the next page, or `None` if this was the last page.
|
||||
pub maybe_next_offset: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct TopSpenderEntry {
|
||||
pub user_token: UserToken,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub email_gravatar_hash: String,
|
||||
/// Sum of positive amounts in the window.
|
||||
pub gross_spend_usd_cents: u64,
|
||||
/// Refund/chargeback magnitude in the window (positive number).
|
||||
pub refund_usd_cents: u64,
|
||||
/// Gross minus refunds. Can be negative if refunds outweigh purchases.
|
||||
pub net_spend_usd_cents: i64,
|
||||
/// Number of positive money events in the window.
|
||||
pub payment_count: u64,
|
||||
/// Credits granted in the window.
|
||||
pub credits_granted: u64,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod list;
|
||||
+1
@@ -1,2 +1,3 @@
|
||||
pub mod omni_gen_audio_cost_and_generate_request;
|
||||
pub mod omni_gen_image_cost_and_generate_request;
|
||||
pub mod omni_gen_video_cost_and_generate_request;
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::common::generation::common_audio_model::CommonAudioModel;
|
||||
use enums::common::generation::common_musical_key::CommonMusicalKey;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Shared request body for both the audio cost estimate and audio generation endpoints.
|
||||
#[derive(Clone, Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct OmniGenAudioCostAndGenerateRequest {
|
||||
/// REQUIRED (even if marked optional)
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub idempotency_token: Option<String>,
|
||||
|
||||
/// REQUIRED (even if marked optional)
|
||||
/// Which model to use.
|
||||
pub model: Option<CommonAudioModel>,
|
||||
|
||||
/// The text prompt for the audio generation.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Style/genre direction (Suno's "tags").
|
||||
pub style_prompt: Option<String>,
|
||||
|
||||
/// Reference audio (optional).
|
||||
/// The remix/sample source, or Seed Audio @Audio references (up to 3).
|
||||
pub audio_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Reference image (optional).
|
||||
/// Seed Audio supports a single reference image; cannot be combined with audio references.
|
||||
pub image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Whether to keep the original lyrics (Suno Remix).
|
||||
pub keep_lyrics: Option<bool>,
|
||||
|
||||
/// Whether to generate instrumental-only audio (Suno Music / Sample).
|
||||
pub is_instrumental: Option<bool>,
|
||||
|
||||
/// Whether the sound should loop vs a single hit (Suno Sounds).
|
||||
pub is_loopable: Option<bool>,
|
||||
|
||||
/// Beats per minute (Suno Sounds).
|
||||
pub bpm: Option<u16>,
|
||||
|
||||
/// The musical key to use (Suno Sounds).
|
||||
pub musical_key: Option<CommonMusicalKey>,
|
||||
|
||||
/// Output sample rate in Hz (Seed Audio: 8000/16000/24000/32000/44100/48000).
|
||||
pub sample_rate_hz: Option<u32>,
|
||||
|
||||
/// Playback speed multiplier (Seed Audio: 0.5–2.0).
|
||||
pub speed: Option<f64>,
|
||||
|
||||
/// Volume multiplier (Seed Audio: 0.5–2.0).
|
||||
pub volume: Option<f64>,
|
||||
|
||||
/// Pitch shift in semitones (Seed Audio: -12..=12).
|
||||
pub pitch: Option<f64>,
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod omni_gen_audio_cost_response;
|
||||
pub mod omni_gen_image_cost_response;
|
||||
pub mod omni_gen_video_cost_response;
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response body for the omni-gen audio cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniGenAudioCostResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// Estimated cost in credits.
|
||||
pub cost_in_credits: Option<u64>,
|
||||
|
||||
/// Estimated cost in USD cents.
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
|
||||
/// Whether the generation is free for this user/plan.
|
||||
pub is_free: bool,
|
||||
|
||||
/// Whether the user has unlimited generations.
|
||||
pub is_unlimited: bool,
|
||||
|
||||
/// Whether the user is rate-limited.
|
||||
pub is_rate_limited: bool,
|
||||
|
||||
/// Whether the output will have a watermark.
|
||||
pub has_watermark: bool,
|
||||
|
||||
/// Whether failures are refunded.
|
||||
/// True: 100% yes
|
||||
/// False: 100% no
|
||||
/// None: Unknown or variable
|
||||
pub failures_are_refunded: Option<bool>,
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod omni_gen_audio_generate_response;
|
||||
pub mod omni_gen_image_generate_response;
|
||||
pub mod omni_gen_video_generate_response;
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response body for the omni-gen audio generation endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniGenAudioGenerateResponse {
|
||||
pub success: bool,
|
||||
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
|
||||
/// All job tokens created by this request (including the primary).
|
||||
/// For single-job requests this will contain just one element matching
|
||||
/// `inference_job_token`. For batch requests this will contain all of them.
|
||||
pub all_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod omni_gen_audio_models;
|
||||
pub mod omni_gen_image_models;
|
||||
pub mod omni_gen_video_models;
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
use enums::common::generation::common_audio_model::CommonAudioModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use serde_derive::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Audio model to default to if none is specified
|
||||
const DEFAULT_AUDIO_MODEL : CommonAudioModel = CommonAudioModel::SunoMusic;
|
||||
|
||||
/// Response body for the audio models endpoint.
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenAudioModelsResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// A list of all models: details, features, and capabilities
|
||||
pub models: Vec<OmniGenAudioModelDetails>,
|
||||
|
||||
/// Provider-by-provider model offering and capability list,
|
||||
/// with possible capability overrides (future)
|
||||
pub providers: Vec<OmniGenAudioModelProviderDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenAudioModelProviderDetails {
|
||||
pub provider: GenerationProvider,
|
||||
pub models: Vec<OmniGenAudioProviderModelDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenAudioProviderModelDetails {
|
||||
pub model: CommonAudioModel,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub overrides: Option<OmniGenAudioModelDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenAudioModelDetails {
|
||||
|
||||
pub model: CommonAudioModel,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model_creator: Option<ModelCreator>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub full_name: Option<String>,
|
||||
|
||||
/// Additional details about the model.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_info: Option<String>,
|
||||
|
||||
/// Additional details about the model. (Brief; only a few words.)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_info_short: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text_prompt_supported: Option<bool>,
|
||||
|
||||
/// Whether a style/genre prompt (Suno's "tags") is supported.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub style_prompt_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub audio_references_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub audio_references_max: Option<u16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_references_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_references_max: Option<u16>,
|
||||
|
||||
/// Whether the "keep lyrics" toggle is supported (Suno Remix).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub keep_lyrics_supported: Option<bool>,
|
||||
|
||||
/// Whether the instrumental-only toggle is supported (Suno Music / Sample).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub instrumental_toggle_supported: Option<bool>,
|
||||
|
||||
/// Whether the loop vs single-hit toggle is supported (Suno Sounds).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub loopable_toggle_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bpm_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub musical_key_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sample_rate_hz_options: Option<Vec<u32>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sample_rate_hz_default: Option<u32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub speed_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub volume_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pitch_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_disabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for OmniGenAudioModelDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: DEFAULT_AUDIO_MODEL,
|
||||
model_creator: None,
|
||||
full_name: None,
|
||||
extra_info: None,
|
||||
extra_info_short: None,
|
||||
text_prompt_supported: None,
|
||||
style_prompt_supported: None,
|
||||
audio_references_supported: None,
|
||||
audio_references_max: None,
|
||||
image_references_supported: None,
|
||||
image_references_max: None,
|
||||
keep_lyrics_supported: None,
|
||||
instrumental_toggle_supported: None,
|
||||
loopable_toggle_supported: None,
|
||||
bpm_supported: None,
|
||||
musical_key_supported: None,
|
||||
sample_rate_hz_options: None,
|
||||
sample_rate_hz_default: None,
|
||||
speed_supported: None,
|
||||
volume_supported: None,
|
||||
pitch_supported: None,
|
||||
is_disabled: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod omni_gen_audio;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_audio_cost_and_generate_request::OmniGenAudioCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::generate_response::omni_gen_audio_generate_response::OmniGenAudioGenerateResponse;
|
||||
|
||||
pub const OMNI_GEN_AUDIO_GENERATE_PATH: &str = "/v1/omni_gen/generate/audio";
|
||||
|
||||
pub async fn omni_gen_audio_generate(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: OmniGenAudioCostAndGenerateRequest,
|
||||
) -> Result<OmniGenAudioGenerateResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
OMNI_GEN_AUDIO_GENERATE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod audio;
|
||||
pub mod image;
|
||||
pub mod video;
|
||||
|
||||
@@ -39,6 +39,7 @@ uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
test_data.workspace = true
|
||||
test_utils.workspace = true
|
||||
strum.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod character_list_ref;
|
||||
pub mod image_list_ref;
|
||||
pub mod image_ref;
|
||||
pub mod router_aspect_ratio;
|
||||
pub mod router_audio_model;
|
||||
pub mod router_bitrate;
|
||||
pub mod router_image_model;
|
||||
pub mod router_provider;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Common audio models supported by the router.
|
||||
/// Not all models are available through all providers.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RouterAudioModel {
|
||||
#[serde(rename = "suno_music")]
|
||||
SunoMusic,
|
||||
|
||||
#[serde(rename = "suno_remix")]
|
||||
SunoRemix,
|
||||
|
||||
#[serde(rename = "suno_sounds")]
|
||||
SunoSounds,
|
||||
|
||||
#[serde(rename = "suno_sample")]
|
||||
SunoSample,
|
||||
|
||||
#[serde(rename = "seed_audio_1p0")]
|
||||
SeedAudio1p0,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// NB: These strings must match `CommonAudioModel` — the two enums convert
|
||||
// via serde string round-trip.
|
||||
#[test]
|
||||
fn all_variants_serialize_to_common_audio_model_strings() {
|
||||
assert_serde_round_trip(RouterAudioModel::SunoMusic, "suno_music");
|
||||
assert_serde_round_trip(RouterAudioModel::SunoRemix, "suno_remix");
|
||||
assert_serde_round_trip(RouterAudioModel::SunoSounds, "suno_sounds");
|
||||
assert_serde_round_trip(RouterAudioModel::SunoSample, "suno_sample");
|
||||
assert_serde_round_trip(RouterAudioModel::SeedAudio1p0, "seed_audio_1p0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_through_common_audio_model() {
|
||||
use enums::common::generation::common_audio_model::CommonAudioModel;
|
||||
|
||||
let cases = [
|
||||
(RouterAudioModel::SunoMusic, CommonAudioModel::SunoMusic),
|
||||
(RouterAudioModel::SunoRemix, CommonAudioModel::SunoRemix),
|
||||
(RouterAudioModel::SunoSounds, CommonAudioModel::SunoSounds),
|
||||
(RouterAudioModel::SunoSample, CommonAudioModel::SunoSample),
|
||||
(RouterAudioModel::SeedAudio1p0, CommonAudioModel::SeedAudio1p0),
|
||||
];
|
||||
for (router_model, expected_common) in cases {
|
||||
let json = serde_json::to_string(&router_model).unwrap();
|
||||
let common: CommonAudioModel = serde_json::from_str(&json)
|
||||
.unwrap_or_else(|e| panic!("CommonAudioModel failed to parse {json}: {e}"));
|
||||
assert_eq!(common, expected_common, "for {router_model:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_serde_round_trip(model: RouterAudioModel, expected: &str) {
|
||||
let json = serde_json::to_string(&model).unwrap();
|
||||
assert_eq!(json, format!("\"{expected}\""));
|
||||
let parsed: RouterAudioModel = serde_json::from_str(&json).unwrap();
|
||||
// RouterAudioModel isn't PartialEq, so round-trip back to the wire form.
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), json);
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,15 @@ pub enum RouterVideoModel {
|
||||
#[serde(rename = "veo_3p1_fast")]
|
||||
Veo3p1Fast,
|
||||
|
||||
#[serde(rename = "veo_3p1_lite")]
|
||||
Veo3p1Lite,
|
||||
|
||||
#[serde(rename = "vidu_q3")]
|
||||
ViduQ3,
|
||||
|
||||
#[serde(rename = "vidu_q3_turbo")]
|
||||
ViduQ3Turbo,
|
||||
|
||||
#[serde(rename = "preview_model")]
|
||||
PreviewModel,
|
||||
|
||||
@@ -123,4 +132,13 @@ mod tests {
|
||||
assert_serde_round_trip(RouterVideoModel::Seedance2p0BytePlusMini, "seedance_2p0_bp_mini");
|
||||
assert_serde_round_trip(RouterVideoModel::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn veo_3p1_lite_and_vidu_variants_serialize() {
|
||||
// NB: These strings must match `CommonVideoModel` — the two enums convert
|
||||
// via serde string round-trip in the omni handlers.
|
||||
assert_serde_round_trip(RouterVideoModel::Veo3p1Lite, "veo_3p1_lite");
|
||||
assert_serde_round_trip(RouterVideoModel::ViduQ3, "vidu_q3");
|
||||
assert_serde_round_trip(RouterVideoModel::ViduQ3Turbo, "vidu_q3_turbo");
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/// The result of estimating the cost of an audio generation plan.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AudioGenerationCostEstimate {
|
||||
pub cost_in_credits: Option<u64>,
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
pub is_free: bool,
|
||||
pub is_unlimited: bool,
|
||||
pub is_rate_limited: bool,
|
||||
pub has_watermark: bool,
|
||||
|
||||
/// Whether failures are refunded.
|
||||
/// True: 100% yes
|
||||
/// False: 100% no
|
||||
/// None: Unknown or variable
|
||||
pub failures_are_refunded: Option<bool>,
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
|
||||
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
|
||||
use crate::generate::generate_audio::audio_generation_request::AudioGenerationRequest;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_remix::cost::KinoviSunoRemixCostState;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_remix::draft::KinoviSunoRemixDraftState;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_sample::cost::KinoviSunoSampleCostState;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_sample::draft::KinoviSunoSampleDraftState;
|
||||
|
||||
/**
|
||||
* Wrapper for all audio generation draft requests.
|
||||
*
|
||||
* Only the Kinovi remix/sample models need a draft phase: they take a user
|
||||
* audio reference that must be downloaded from our CDN and re-uploaded to the
|
||||
* Kinovi CDN before the request can be sent. All other audio providers return
|
||||
* a `Request` directly from `build2()`.
|
||||
*/
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AudioGenerationDraftRequest {
|
||||
KinoviSunoRemix(KinoviSunoRemixDraftState),
|
||||
KinoviSunoSample(KinoviSunoSampleDraftState),
|
||||
}
|
||||
|
||||
impl AudioGenerationDraftRequest {
|
||||
|
||||
pub fn get_provider(&self) -> RouterProvider {
|
||||
match self {
|
||||
Self::KinoviSunoRemix(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSunoSample(_) => RouterProvider::Seedance2Pro,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a cost estimate to fulfill the request.
|
||||
pub fn estimate_cost(&self) -> Result<AudioGenerationCostEstimate, ArtcraftRouterError> {
|
||||
match self {
|
||||
AudioGenerationDraftRequest::KinoviSunoRemix(draft) => Ok(KinoviSunoRemixCostState::from_draft(draft).estimate_cost()),
|
||||
AudioGenerationDraftRequest::KinoviSunoSample(draft) => Ok(KinoviSunoSampleCostState::from_draft(draft).estimate_cost()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Finalize the draft request before generation
|
||||
/// This may involve uploading media to the provider.
|
||||
pub async fn finalize(self, draft_context: AudioGenerationDraftContext<'_>) -> Result<AudioGenerationRequest, ArtcraftRouterError> {
|
||||
match self {
|
||||
AudioGenerationDraftRequest::KinoviSunoRemix(mut draft) => {
|
||||
let result = draft.to_request(&draft_context).await?;
|
||||
Ok(AudioGenerationRequest::KinoviSunoRemix(result))
|
||||
},
|
||||
AudioGenerationDraftRequest::KinoviSunoSample(mut draft) => {
|
||||
let result = draft.to_request(&draft_context).await?;
|
||||
Ok(AudioGenerationRequest::KinoviSunoSample(result))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct AudioGenerationDraftContext<'a> {
|
||||
/// Optional: the router client, needed for providers that require authentication.
|
||||
pub client: Option<&'a RouterClient>,
|
||||
|
||||
/// Optional context: a map of Media File Tokens to their ArtCraft URLs
|
||||
/// Only needed if we have to fetch these assets and upload them to another provider.
|
||||
pub media_file_to_artcraft_url_map: Option<&'a HashMap<MediaFileToken, String>>,
|
||||
}
|
||||
|
||||
impl <'a> AudioGenerationDraftContext<'a> {
|
||||
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ArtcraftRouterError> {
|
||||
let client = self.client.ok_or(ArtcraftRouterError::Client(ClientError::RouterClientNotProvided))?;
|
||||
client.get_seedance2pro_client_ref()
|
||||
.map_err(|err| ArtcraftRouterError::Client(err))
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for AudioGenerationDraftContext<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AudioGenerationDraftContext")
|
||||
.field("client", &self.client.is_some())
|
||||
.field("media_file_to_artcraft_url_map", &self.media_file_to_artcraft_url_map)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
|
||||
use crate::generate::generate_audio::audio_generation_draft::AudioGenerationDraftRequest;
|
||||
use crate::generate::generate_audio::audio_generation_request::AudioGenerationRequest;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AudioGenerationDraftOrRequest {
|
||||
Draft(AudioGenerationDraftRequest),
|
||||
Request(AudioGenerationRequest),
|
||||
}
|
||||
|
||||
impl AudioGenerationDraftOrRequest {
|
||||
|
||||
pub fn get_provider(&self) -> RouterProvider {
|
||||
match self {
|
||||
Self::Draft(draft) => draft.get_provider(),
|
||||
Self::Request(request) => request.get_provider(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> Result<AudioGenerationCostEstimate, ArtcraftRouterError> {
|
||||
match self {
|
||||
AudioGenerationDraftOrRequest::Draft(draft) => draft.estimate_cost(),
|
||||
AudioGenerationDraftOrRequest::Request(request) => request.estimate_cost(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
use crate::generate::generate_audio::providers::artcraft::seed_audio_1p0::cost::ArtcraftSeedAudio1p0CostState;
|
||||
use crate::generate::generate_audio::providers::artcraft::seed_audio_1p0::request::ArtcraftSeedAudio1p0RequestState;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_music::cost::ArtcraftSunoMusicCostState;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_music::request::ArtcraftSunoMusicRequestState;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_remix::cost::ArtcraftSunoRemixCostState;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_remix::request::ArtcraftSunoRemixRequestState;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_sample::cost::ArtcraftSunoSampleCostState;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_sample::request::ArtcraftSunoSampleRequestState;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_sounds::cost::ArtcraftSunoSoundsCostState;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_sounds::request::ArtcraftSunoSoundsRequestState;
|
||||
use crate::generate::generate_audio::providers::fal::seed_audio_1p0::cost::FalSeedAudio1p0CostState;
|
||||
use crate::generate::generate_audio::providers::fal::seed_audio_1p0::request::FalSeedAudio1p0RequestState;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_music::cost::KinoviSunoMusicCostState;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_music::request::KinoviSunoMusicRequestState;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_remix::cost::KinoviSunoRemixCostState;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_remix::request::KinoviSunoRemixRequestState;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_sample::cost::KinoviSunoSampleCostState;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_sample::request::KinoviSunoSampleRequestState;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_sounds::cost::KinoviSunoSoundsCostState;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_sounds::request::KinoviSunoSoundsRequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AudioGenerationRequest {
|
||||
ArtcraftSunoMusic(ArtcraftSunoMusicRequestState),
|
||||
ArtcraftSunoRemix(ArtcraftSunoRemixRequestState),
|
||||
ArtcraftSunoSounds(ArtcraftSunoSoundsRequestState),
|
||||
ArtcraftSunoSample(ArtcraftSunoSampleRequestState),
|
||||
ArtcraftSeedAudio1p0(ArtcraftSeedAudio1p0RequestState),
|
||||
FalSeedAudio1p0(FalSeedAudio1p0RequestState),
|
||||
KinoviSunoMusic(KinoviSunoMusicRequestState),
|
||||
KinoviSunoRemix(KinoviSunoRemixRequestState),
|
||||
KinoviSunoSounds(KinoviSunoSoundsRequestState),
|
||||
KinoviSunoSample(KinoviSunoSampleRequestState),
|
||||
}
|
||||
|
||||
impl AudioGenerationRequest {
|
||||
|
||||
pub fn get_provider(&self) -> RouterProvider {
|
||||
match self {
|
||||
Self::ArtcraftSunoMusic(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftSunoRemix(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftSunoSounds(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftSunoSample(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftSeedAudio1p0(_) => RouterProvider::Artcraft,
|
||||
Self::FalSeedAudio1p0(_) => RouterProvider::Fal,
|
||||
Self::KinoviSunoMusic(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSunoRemix(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSunoSounds(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSunoSample(_) => RouterProvider::Seedance2Pro,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a cost estimate to fulfill the request.
|
||||
pub fn estimate_cost(&self) -> Result<AudioGenerationCostEstimate, ArtcraftRouterError> {
|
||||
match self {
|
||||
AudioGenerationRequest::ArtcraftSunoMusic(request) => Ok(ArtcraftSunoMusicCostState::from_request(request).estimate_cost()),
|
||||
AudioGenerationRequest::ArtcraftSunoRemix(request) => Ok(ArtcraftSunoRemixCostState::from_request(request).estimate_cost()),
|
||||
AudioGenerationRequest::ArtcraftSunoSounds(request) => Ok(ArtcraftSunoSoundsCostState::from_request(request).estimate_cost()),
|
||||
AudioGenerationRequest::ArtcraftSunoSample(request) => Ok(ArtcraftSunoSampleCostState::from_request(request).estimate_cost()),
|
||||
AudioGenerationRequest::ArtcraftSeedAudio1p0(request) => Ok(ArtcraftSeedAudio1p0CostState::from_request(request).estimate_cost()),
|
||||
AudioGenerationRequest::FalSeedAudio1p0(request) => Ok(FalSeedAudio1p0CostState::from_request(request).estimate_cost()),
|
||||
AudioGenerationRequest::KinoviSunoMusic(request) => Ok(KinoviSunoMusicCostState::from_request(request).estimate_cost()),
|
||||
AudioGenerationRequest::KinoviSunoRemix(request) => Ok(KinoviSunoRemixCostState::from_request(request).estimate_cost()),
|
||||
AudioGenerationRequest::KinoviSunoSounds(request) => Ok(KinoviSunoSoundsCostState::from_request(request).estimate_cost()),
|
||||
AudioGenerationRequest::KinoviSunoSample(request) => Ok(KinoviSunoSampleCostState::from_request(request).estimate_cost()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send the audio generation request
|
||||
/// If successful, returns the job IDs.
|
||||
pub async fn send_request(&self, client: &RouterClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
AudioGenerationRequest::ArtcraftSunoMusic(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
AudioGenerationRequest::ArtcraftSunoRemix(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
AudioGenerationRequest::ArtcraftSunoSounds(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
AudioGenerationRequest::ArtcraftSunoSample(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
AudioGenerationRequest::ArtcraftSeedAudio1p0(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
AudioGenerationRequest::FalSeedAudio1p0(request) => {
|
||||
let client_ref = client.get_fal_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
AudioGenerationRequest::KinoviSunoMusic(request) => {
|
||||
let client_ref = client.get_seedance2pro_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
AudioGenerationRequest::KinoviSunoRemix(request) => {
|
||||
let client_ref = client.get_seedance2pro_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
AudioGenerationRequest::KinoviSunoSounds(request) => {
|
||||
let client_ref = client.get_seedance2pro_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
AudioGenerationRequest::KinoviSunoSample(request) => {
|
||||
let client_ref = client.get_seedance2pro_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
use enums::common::generation::common_musical_key::CommonMusicalKey;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::providers::artcraft::seed_audio_1p0::build::build_artcraft_seed_audio_1p0;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_music::build::build_artcraft_suno_music;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_remix::build::build_artcraft_suno_remix;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_sample::build::build_artcraft_suno_sample;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_sounds::build::build_artcraft_suno_sounds;
|
||||
use crate::generate::generate_audio::providers::fal::seed_audio_1p0::build::build_fal_seed_audio_1p0;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_music::build::build_kinovi_suno_music;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_remix::build::build_kinovi_suno_remix;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_sample::build::build_kinovi_suno_sample;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_sounds::build::build_kinovi_suno_sounds;
|
||||
|
||||
/// RouterProvider-agnostic audio generation request. Distilled by `build2()`
|
||||
/// into an `AudioGenerationDraftOrRequest` for the selected (provider, model)
|
||||
/// pair.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateAudioRequestBuilder {
|
||||
/// Which model to use.
|
||||
pub model: RouterAudioModel,
|
||||
|
||||
/// Which provider to use.
|
||||
pub provider: RouterProvider,
|
||||
|
||||
/// The prompt for the audio generation.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Style/genre direction (Suno's "tags"), e.g. "EDM style meets dance".
|
||||
pub style_prompt: Option<String>,
|
||||
|
||||
/// Reference audio (optional).
|
||||
/// The remix/sample source, or Seed Audio @Audio references (up to 3).
|
||||
pub audio_references: Option<AudioListRef>,
|
||||
|
||||
/// Reference images (optional).
|
||||
/// Seed Audio supports a single reference image; it cannot be combined
|
||||
/// with audio references.
|
||||
pub image_references: Option<ImageListRef>,
|
||||
|
||||
/// Whether to keep the original lyrics (Suno Remix).
|
||||
pub keep_lyrics: Option<bool>,
|
||||
|
||||
/// Whether to generate instrumental-only audio (Suno Music / Sample).
|
||||
pub is_instrumental: Option<bool>,
|
||||
|
||||
/// Whether the sound should loop vs a single hit (Suno Sounds).
|
||||
pub is_loopable: Option<bool>,
|
||||
|
||||
/// Beats per minute (Suno Sounds).
|
||||
pub bpm: Option<u16>,
|
||||
|
||||
/// The musical key to use (Suno Sounds).
|
||||
pub musical_key: Option<CommonMusicalKey>,
|
||||
|
||||
/// Output sample rate in Hz (Seed Audio).
|
||||
pub sample_rate_hz: Option<u32>,
|
||||
|
||||
/// Playback speed multiplier (Seed Audio: 0.5–2.0).
|
||||
pub speed: Option<f64>,
|
||||
|
||||
/// Volume multiplier (Seed Audio: 0.5–2.0).
|
||||
pub volume: Option<f64>,
|
||||
|
||||
/// Pitch shift in semitones (Seed Audio: -12..=12).
|
||||
pub pitch: Option<f64>,
|
||||
|
||||
/// If the request is a mismatch with the (model/provider), how to mitigate it.
|
||||
pub request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy,
|
||||
|
||||
/// Some providers support idempotency.
|
||||
/// If not supplied, we'll generate one for the required providers.
|
||||
pub idempotency_token: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for GenerateAudioRequestBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: RouterAudioModel::SunoMusic,
|
||||
provider: RouterProvider::Artcraft,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
prompt: None,
|
||||
style_prompt: None,
|
||||
audio_references: None,
|
||||
image_references: None,
|
||||
keep_lyrics: None,
|
||||
is_instrumental: None,
|
||||
is_loopable: None,
|
||||
bpm: None,
|
||||
musical_key: None,
|
||||
sample_rate_hz: None,
|
||||
speed: None,
|
||||
volume: None,
|
||||
pitch: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateAudioRequestBuilder {
|
||||
|
||||
pub fn build2(self) -> Result<AudioGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
match (self.provider, self.model) {
|
||||
// Artcraft
|
||||
(RouterProvider::Artcraft, RouterAudioModel::SunoMusic) => build_artcraft_suno_music(self),
|
||||
(RouterProvider::Artcraft, RouterAudioModel::SunoRemix) => build_artcraft_suno_remix(self),
|
||||
(RouterProvider::Artcraft, RouterAudioModel::SunoSounds) => build_artcraft_suno_sounds(self),
|
||||
(RouterProvider::Artcraft, RouterAudioModel::SunoSample) => build_artcraft_suno_sample(self),
|
||||
(RouterProvider::Artcraft, RouterAudioModel::SeedAudio1p0) => build_artcraft_seed_audio_1p0(self),
|
||||
// Fal
|
||||
(RouterProvider::Fal, RouterAudioModel::SeedAudio1p0) => build_fal_seed_audio_1p0(self),
|
||||
// Kinovi
|
||||
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoMusic) => build_kinovi_suno_music(self),
|
||||
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoRemix) => build_kinovi_suno_remix(self),
|
||||
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSounds) => build_kinovi_suno_sounds(self),
|
||||
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSample) => build_kinovi_suno_sample(self),
|
||||
_ => self.unsupported_provider_and_model(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_generate_idempotency_token(&self) -> String {
|
||||
self.idempotency_token.clone()
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
|
||||
}
|
||||
|
||||
fn unsupported_provider_and_model(&self) -> Result<AudioGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(
|
||||
format!("Audio generation for model `{:?}` is not supported for provider {:?}", self.model, self.provider)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::generate::generate_audio::audio_generation_draft::AudioGenerationDraftRequest;
|
||||
use crate::generate::generate_audio::audio_generation_request::AudioGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod dispatch_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn artcraft_suno_music_dispatches() {
|
||||
let result = builder(RouterProvider::Artcraft, RouterAudioModel::SunoMusic).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::ArtcraftSunoMusic(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artcraft_suno_remix_dispatches() {
|
||||
let result = builder_with_audio_token_ref(RouterProvider::Artcraft, RouterAudioModel::SunoRemix).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::ArtcraftSunoRemix(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artcraft_suno_sounds_dispatches() {
|
||||
let result = builder(RouterProvider::Artcraft, RouterAudioModel::SunoSounds).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::ArtcraftSunoSounds(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artcraft_suno_sample_dispatches() {
|
||||
let result = builder_with_audio_token_ref(RouterProvider::Artcraft, RouterAudioModel::SunoSample).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::ArtcraftSunoSample(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artcraft_seed_audio_1p0_dispatches() {
|
||||
let result = builder(RouterProvider::Artcraft, RouterAudioModel::SeedAudio1p0).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::ArtcraftSeedAudio1p0(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fal_seed_audio_1p0_dispatches() {
|
||||
let result = builder(RouterProvider::Fal, RouterAudioModel::SeedAudio1p0).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::FalSeedAudio1p0(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kinovi_suno_music_dispatches_to_request() {
|
||||
let result = builder(RouterProvider::Seedance2Pro, RouterAudioModel::SunoMusic).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::KinoviSunoMusic(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kinovi_suno_sounds_dispatches_to_request() {
|
||||
let result = builder(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSounds).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::KinoviSunoSounds(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kinovi_suno_remix_dispatches_to_draft() {
|
||||
let result = builder_with_audio_ref(RouterProvider::Seedance2Pro, RouterAudioModel::SunoRemix).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Draft(AudioGenerationDraftRequest::KinoviSunoRemix(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kinovi_suno_sample_dispatches_to_draft() {
|
||||
let result = builder_with_audio_ref(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSample).build2().expect("build");
|
||||
assert!(matches!(
|
||||
result,
|
||||
AudioGenerationDraftOrRequest::Draft(AudioGenerationDraftRequest::KinoviSunoSample(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
mod unsupported_combo_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn kinovi_seed_audio_1p0_is_unsupported() {
|
||||
let result = builder(RouterProvider::Seedance2Pro, RouterAudioModel::SeedAudio1p0).build2();
|
||||
assert!(matches!(result, Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fal_suno_models_are_unsupported() {
|
||||
for model in [
|
||||
RouterAudioModel::SunoMusic,
|
||||
RouterAudioModel::SunoRemix,
|
||||
RouterAudioModel::SunoSounds,
|
||||
RouterAudioModel::SunoSample,
|
||||
] {
|
||||
let result = builder(RouterProvider::Fal, model).build2();
|
||||
assert!(
|
||||
matches!(result, Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(_))),
|
||||
"expected unsupported error for Fal + {model:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gmicloud_and_grok_are_unsupported() {
|
||||
for provider in [RouterProvider::GmiCloud, RouterProvider::GrokApi] {
|
||||
let result = builder(provider, RouterAudioModel::SunoMusic).build2();
|
||||
assert!(
|
||||
matches!(result, Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(_))),
|
||||
"expected unsupported error for {provider:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn builder(provider: RouterProvider, model: RouterAudioModel) -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
provider,
|
||||
model,
|
||||
prompt: Some("a song about corgis".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn builder_with_audio_ref(provider: RouterProvider, model: RouterAudioModel) -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.mp3".to_string()])),
|
||||
..builder(provider, model)
|
||||
}
|
||||
}
|
||||
|
||||
fn builder_with_audio_token_ref(provider: RouterProvider, model: RouterAudioModel) -> GenerateAudioRequestBuilder {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
GenerateAudioRequestBuilder {
|
||||
audio_references: Some(AudioListRef::MediaFileTokens(vec![MediaFileToken::new("mf_test123".to_string())])),
|
||||
..builder(provider, model)
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftAudioResponsePayload {
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
pub all_inference_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Seedance2proAudioResponsePayload {
|
||||
pub order_id: String,
|
||||
pub task_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalAudioResponsePayload {
|
||||
pub request_id: Option<String>,
|
||||
pub gateway_request_id: Option<String>,
|
||||
|
||||
/// Queue-mode status URL (fal's queue/polling flow). `None` for webhook
|
||||
/// dispatch — the webhook callback drives status updates instead.
|
||||
pub maybe_status_url: Option<String>,
|
||||
|
||||
/// Queue-mode response URL (fal's queue/polling flow). `None` for webhook
|
||||
/// dispatch.
|
||||
pub maybe_response_url: Option<String>,
|
||||
|
||||
/// The outbound request that was sent to Fal.
|
||||
/// Stored as a trait object so any Request type can be captured.
|
||||
/// Use `format!("{:?}", ...)` or `format!("{:#?}", ...)` to print.
|
||||
pub maybe_outbound_request: Option<Arc<dyn Debug + Send + Sync>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum GenerateAudioResponse {
|
||||
Artcraft(ArtcraftAudioResponsePayload),
|
||||
Seedance2Pro(Seedance2proAudioResponsePayload),
|
||||
Fal(FalAudioResponsePayload),
|
||||
}
|
||||
|
||||
impl GenerateAudioResponse {
|
||||
pub fn get_artcraft_payload(&self) -> Option<ArtcraftAudioResponsePayload> {
|
||||
match self {
|
||||
Self::Artcraft(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proAudioResponsePayload> {
|
||||
match self {
|
||||
Self::Seedance2Pro(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fal_payload(&self) -> Option<FalAudioResponsePayload> {
|
||||
match self {
|
||||
Self::Fal(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod audio_generation_cost_estimate;
|
||||
pub mod audio_generation_draft;
|
||||
pub mod audio_generation_draft_context;
|
||||
pub mod audio_generation_draft_or_request;
|
||||
pub mod audio_generation_request;
|
||||
pub mod generate_audio_request_builder;
|
||||
pub mod generate_audio_response;
|
||||
pub mod providers;
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_audio_cost_and_generate_request::OmniGenAudioCostAndGenerateRequest;
|
||||
use enums::common::generation::common_audio_model::CommonAudioModel as CommonAudioModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::providers::artcraft::resolve::{
|
||||
resolve_audio_list_ref, resolve_image_list_ref,
|
||||
};
|
||||
|
||||
/// Build an `OmniGenAudioCostAndGenerateRequest` from the builder. The
|
||||
/// Artcraft provider is a thin forwarding shim: the storyteller-web omni
|
||||
/// audio endpoint owns per-model validation and option mapping, so the
|
||||
/// builder's options are passed through as-is.
|
||||
pub fn build_artcraft_omni_audio_request(
|
||||
mut builder: GenerateAudioRequestBuilder,
|
||||
model: CommonAudioModelEnum,
|
||||
) -> Result<OmniGenAudioCostAndGenerateRequest, ArtcraftRouterError> {
|
||||
let audio_media_tokens = resolve_audio_list_ref(builder.audio_references.take())?;
|
||||
let image_media_tokens = resolve_image_list_ref(builder.image_references.take())?;
|
||||
let idempotency_token = builder.get_or_generate_idempotency_token();
|
||||
|
||||
Ok(OmniGenAudioCostAndGenerateRequest {
|
||||
idempotency_token: Some(idempotency_token),
|
||||
model: Some(model),
|
||||
prompt: builder.prompt.take(),
|
||||
style_prompt: builder.style_prompt.take(),
|
||||
audio_media_tokens,
|
||||
image_media_tokens,
|
||||
keep_lyrics: builder.keep_lyrics,
|
||||
is_instrumental: builder.is_instrumental,
|
||||
is_loopable: builder.is_loopable,
|
||||
bpm: builder.bpm,
|
||||
musical_key: builder.musical_key,
|
||||
sample_rate_hz: builder.sample_rate_hz,
|
||||
speed: builder.speed,
|
||||
volume: builder.volume,
|
||||
pitch: builder.pitch,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_fields_are_forwarded() {
|
||||
use enums::common::generation::common_musical_key::CommonMusicalKey;
|
||||
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
prompt: Some("a song".to_string()),
|
||||
style_prompt: Some("EDM".to_string()),
|
||||
audio_references: Some(AudioListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_audio".to_string()),
|
||||
])),
|
||||
image_references: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_image".to_string()),
|
||||
])),
|
||||
keep_lyrics: Some(true),
|
||||
is_instrumental: Some(false),
|
||||
is_loopable: Some(true),
|
||||
bpm: Some(120),
|
||||
musical_key: Some(CommonMusicalKey::AMinor),
|
||||
sample_rate_hz: Some(48_000),
|
||||
speed: Some(1.5),
|
||||
volume: Some(0.8),
|
||||
pitch: Some(-2.0),
|
||||
idempotency_token: Some("test-token".to_string()),
|
||||
..base_builder()
|
||||
};
|
||||
|
||||
let request = build_artcraft_omni_audio_request(builder, CommonAudioModelEnum::SunoMusic)
|
||||
.expect("build");
|
||||
|
||||
assert_eq!(request.idempotency_token.as_deref(), Some("test-token"));
|
||||
assert_eq!(request.model, Some(CommonAudioModelEnum::SunoMusic));
|
||||
assert_eq!(request.prompt.as_deref(), Some("a song"));
|
||||
assert_eq!(request.style_prompt.as_deref(), Some("EDM"));
|
||||
assert_eq!(request.audio_media_tokens.as_ref().map(|t| t.len()), Some(1));
|
||||
assert_eq!(request.image_media_tokens.as_ref().map(|t| t.len()), Some(1));
|
||||
assert_eq!(request.keep_lyrics, Some(true));
|
||||
assert_eq!(request.is_instrumental, Some(false));
|
||||
assert_eq!(request.is_loopable, Some(true));
|
||||
assert_eq!(request.bpm, Some(120));
|
||||
assert_eq!(request.musical_key, Some(CommonMusicalKey::AMinor));
|
||||
assert_eq!(request.sample_rate_hz, Some(48_000));
|
||||
assert_eq!(request.speed, Some(1.5));
|
||||
assert_eq!(request.volume, Some(0.8));
|
||||
assert_eq!(request.pitch, Some(-2.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idempotency_token_is_generated_when_missing() {
|
||||
let request = build_artcraft_omni_audio_request(base_builder(), CommonAudioModelEnum::SunoMusic)
|
||||
.expect("build");
|
||||
assert!(request.idempotency_token.is_some());
|
||||
assert!(!request.idempotency_token.unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_url_references_are_rejected() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.mp3".to_string()])),
|
||||
..base_builder()
|
||||
};
|
||||
let result = build_artcraft_omni_audio_request(builder, CommonAudioModelEnum::SunoRemix);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_url_references_are_rejected() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
image_references: Some(ImageListRef::Urls(vec!["https://example.com/a.png".to_string()])),
|
||||
..base_builder()
|
||||
};
|
||||
let result = build_artcraft_omni_audio_request(builder, CommonAudioModelEnum::SeedAudio1p0);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoMusic,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("a song".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
pub(crate) mod build_common;
|
||||
pub(crate) mod request_common;
|
||||
pub(crate) mod resolve;
|
||||
pub mod seed_audio_1p0;
|
||||
pub mod suno_music;
|
||||
pub mod suno_remix;
|
||||
pub mod suno_sample;
|
||||
pub mod suno_sounds;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_audio_cost_and_generate_request::OmniGenAudioCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::audio::omni_gen_audio::omni_gen_audio_generate;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_audio::generate_audio_response::{
|
||||
ArtcraftAudioResponsePayload, GenerateAudioResponse,
|
||||
};
|
||||
|
||||
/// Send an audio generation request to the Artcraft omni-gen endpoint.
|
||||
/// All Artcraft model request states delegate to this function.
|
||||
pub async fn send_artcraft_omni_audio_request(
|
||||
request: &OmniGenAudioCostAndGenerateRequest,
|
||||
client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
let response = omni_gen_audio_generate(
|
||||
&client.api_host,
|
||||
Some(&client.credentials),
|
||||
request.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateAudioResponse::Artcraft(ArtcraftAudioResponsePayload {
|
||||
inference_job_token: response.inference_job_token.clone(),
|
||||
all_inference_job_tokens: vec![response.inference_job_token],
|
||||
}))
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
//! Shared resolve helpers for the Artcraft audio provider.
|
||||
//!
|
||||
//! Artcraft only accepts media file tokens — raw URLs are rejected.
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
pub(super) fn resolve_audio_list_ref(
|
||||
audio_list_ref: Option<AudioListRef>,
|
||||
) -> Result<Option<Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match audio_list_ref {
|
||||
None => Ok(None),
|
||||
Some(AudioListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(AudioListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Option<Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_audio_model::CommonAudioModel as CommonAudioModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::audio_generation_request::AudioGenerationRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::providers::artcraft::build_common::build_artcraft_omni_audio_request;
|
||||
use crate::generate::generate_audio::providers::artcraft::seed_audio_1p0::request::ArtcraftSeedAudio1p0RequestState;
|
||||
|
||||
pub fn build_artcraft_seed_audio_1p0(builder: GenerateAudioRequestBuilder) -> Result<AudioGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_audio_request(builder, CommonAudioModelEnum::SeedAudio1p0)?;
|
||||
let state = ArtcraftSeedAudio1p0RequestState { request };
|
||||
Ok(AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::ArtcraftSeedAudio1p0(state)))
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
|
||||
use crate::generate::generate_audio::providers::artcraft::seed_audio_1p0::request::ArtcraftSeedAudio1p0RequestState;
|
||||
|
||||
const COST_IN_USD_CENTS: u64 = 21;
|
||||
|
||||
/// Seed Audio 1.0 via Artcraft is flat priced, so the cost state carries no
|
||||
/// request fields.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedAudio1p0CostState;
|
||||
|
||||
impl ArtcraftSeedAudio1p0CostState {
|
||||
pub fn from_request(_request: &ArtcraftSeedAudio1p0RequestState) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
|
||||
AudioGenerationCostEstimate {
|
||||
cost_in_credits: Some(COST_IN_USD_CENTS),
|
||||
cost_in_usd_cents: Some(COST_IN_USD_CENTS),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn flat_cost_is_twenty_one_cents() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SeedAudio1p0,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("a suspense radio drama".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let estimate = builder.build2().unwrap().estimate_cost().unwrap();
|
||||
assert_eq!(estimate.cost_in_usd_cents, Some(21));
|
||||
assert_eq!(estimate.cost_in_credits, Some(21));
|
||||
// Direct cost-state path matches the builder path.
|
||||
assert_eq!(ArtcraftSeedAudio1p0CostState.estimate_cost().cost_in_usd_cents, Some(21));
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_audio_cost_and_generate_request::OmniGenAudioCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
use crate::generate::generate_audio::providers::artcraft::request_common::send_artcraft_omni_audio_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedAudio1p0RequestState {
|
||||
pub request: OmniGenAudioCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedAudio1p0RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_audio_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // sends a real generation to the Artcraft backend, incurs cost
|
||||
async fn text_to_speech_drama() {
|
||||
let response = run_pipeline(GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SeedAudio1p0,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("A calm narrator describes ocean waves rolling onto a moonlit beach.".to_string()),
|
||||
..Default::default()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateAudioResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn get_artcraft_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
|
||||
.expect("Failed to read artcraft cookies");
|
||||
let cookies = cookies.trim().to_string();
|
||||
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
|
||||
.expect("Failed to parse cookies")
|
||||
.expect("No credentials found");
|
||||
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
|
||||
let client = get_artcraft_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
AudioGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Artcraft skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateAudioResponse::Artcraft(p) => {
|
||||
println!("inference_job_token={:?}", p.inference_job_token);
|
||||
println!("all_inference_job_tokens={:?}", p.all_inference_job_tokens);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_audio_model::CommonAudioModel as CommonAudioModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::audio_generation_request::AudioGenerationRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::providers::artcraft::build_common::build_artcraft_omni_audio_request;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_music::request::ArtcraftSunoMusicRequestState;
|
||||
|
||||
pub fn build_artcraft_suno_music(builder: GenerateAudioRequestBuilder) -> Result<AudioGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_audio_request(builder, CommonAudioModelEnum::SunoMusic)?;
|
||||
let state = ArtcraftSunoMusicRequestState { request };
|
||||
Ok(AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::ArtcraftSunoMusic(state)))
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_music::request::ArtcraftSunoMusicRequestState;
|
||||
|
||||
const COST_IN_USD_CENTS: u64 = 8;
|
||||
|
||||
/// Suno Music via Artcraft is flat priced, so the cost state carries no
|
||||
/// request fields.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSunoMusicCostState;
|
||||
|
||||
impl ArtcraftSunoMusicCostState {
|
||||
pub fn from_request(_request: &ArtcraftSunoMusicRequestState) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
|
||||
AudioGenerationCostEstimate {
|
||||
cost_in_credits: Some(COST_IN_USD_CENTS),
|
||||
cost_in_usd_cents: Some(COST_IN_USD_CENTS),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn flat_cost_is_eight_cents() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoMusic,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("a song".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let estimate = builder.build2().unwrap().estimate_cost().unwrap();
|
||||
assert_eq!(estimate.cost_in_usd_cents, Some(8));
|
||||
assert_eq!(estimate.cost_in_credits, Some(8));
|
||||
// Direct cost-state path matches the builder path.
|
||||
assert_eq!(ArtcraftSunoMusicCostState.estimate_cost().cost_in_usd_cents, Some(8));
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_audio_cost_and_generate_request::OmniGenAudioCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
use crate::generate::generate_audio::providers::artcraft::request_common::send_artcraft_omni_audio_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSunoMusicRequestState {
|
||||
pub request: OmniGenAudioCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSunoMusicRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_audio_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // sends a real generation to the Artcraft backend, incurs cost
|
||||
async fn text_to_music() {
|
||||
let response = run_pipeline(GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoMusic,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("A song about a corgi who learns to sail the open sea".to_string()),
|
||||
style_prompt: Some("Sea shanty, folk".to_string()),
|
||||
..Default::default()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateAudioResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn get_artcraft_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
|
||||
.expect("Failed to read artcraft cookies");
|
||||
let cookies = cookies.trim().to_string();
|
||||
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
|
||||
.expect("Failed to parse cookies")
|
||||
.expect("No credentials found");
|
||||
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
|
||||
let client = get_artcraft_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
AudioGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Artcraft skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateAudioResponse::Artcraft(p) => {
|
||||
println!("inference_job_token={:?}", p.inference_job_token);
|
||||
println!("all_inference_job_tokens={:?}", p.all_inference_job_tokens);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_audio_model::CommonAudioModel as CommonAudioModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::audio_generation_request::AudioGenerationRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::providers::artcraft::build_common::build_artcraft_omni_audio_request;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_remix::request::ArtcraftSunoRemixRequestState;
|
||||
|
||||
pub fn build_artcraft_suno_remix(builder: GenerateAudioRequestBuilder) -> Result<AudioGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_audio_request(builder, CommonAudioModelEnum::SunoRemix)?;
|
||||
let state = ArtcraftSunoRemixRequestState { request };
|
||||
Ok(AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::ArtcraftSunoRemix(state)))
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_remix::request::ArtcraftSunoRemixRequestState;
|
||||
|
||||
const COST_IN_USD_CENTS: u64 = 8;
|
||||
|
||||
/// Suno Remix via Artcraft is flat priced, so the cost state carries no
|
||||
/// request fields.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSunoRemixCostState;
|
||||
|
||||
impl ArtcraftSunoRemixCostState {
|
||||
pub fn from_request(_request: &ArtcraftSunoRemixRequestState) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
|
||||
AudioGenerationCostEstimate {
|
||||
cost_in_credits: Some(COST_IN_USD_CENTS),
|
||||
cost_in_usd_cents: Some(COST_IN_USD_CENTS),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn flat_cost_is_eight_cents() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoRemix,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("make this electronic".to_string()),
|
||||
audio_references: Some(AudioListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_test123".to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
};
|
||||
let estimate = builder.build2().unwrap().estimate_cost().unwrap();
|
||||
assert_eq!(estimate.cost_in_usd_cents, Some(8));
|
||||
assert_eq!(estimate.cost_in_credits, Some(8));
|
||||
// Direct cost-state path matches the builder path.
|
||||
assert_eq!(ArtcraftSunoRemixCostState.estimate_cost().cost_in_usd_cents, Some(8));
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_audio_cost_and_generate_request::OmniGenAudioCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
use crate::generate::generate_audio::providers::artcraft::request_common::send_artcraft_omni_audio_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSunoRemixRequestState {
|
||||
pub request: OmniGenAudioCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSunoRemixRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_audio_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
|
||||
/// Replace with a real uploaded audio media token before running.
|
||||
/// (test_data doesn't have a production audio media token yet.)
|
||||
const AUDIO_MEDIA_TOKEN: &str = "m_replace_with_uploaded_audio_token";
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // sends a real generation to the Artcraft backend, incurs cost
|
||||
async fn remix_uploaded_audio() {
|
||||
let response = run_pipeline(GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoRemix,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("Make this electronic".to_string()),
|
||||
style_prompt: Some("EDM style".to_string()),
|
||||
audio_references: Some(AudioListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new(AUDIO_MEDIA_TOKEN.to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateAudioResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn get_artcraft_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
|
||||
.expect("Failed to read artcraft cookies");
|
||||
let cookies = cookies.trim().to_string();
|
||||
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
|
||||
.expect("Failed to parse cookies")
|
||||
.expect("No credentials found");
|
||||
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
|
||||
let client = get_artcraft_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
AudioGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Artcraft skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateAudioResponse::Artcraft(p) => {
|
||||
println!("inference_job_token={:?}", p.inference_job_token);
|
||||
println!("all_inference_job_tokens={:?}", p.all_inference_job_tokens);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_audio_model::CommonAudioModel as CommonAudioModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::audio_generation_request::AudioGenerationRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::providers::artcraft::build_common::build_artcraft_omni_audio_request;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_sample::request::ArtcraftSunoSampleRequestState;
|
||||
|
||||
pub fn build_artcraft_suno_sample(builder: GenerateAudioRequestBuilder) -> Result<AudioGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_audio_request(builder, CommonAudioModelEnum::SunoSample)?;
|
||||
let state = ArtcraftSunoSampleRequestState { request };
|
||||
Ok(AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::ArtcraftSunoSample(state)))
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_sample::request::ArtcraftSunoSampleRequestState;
|
||||
|
||||
const COST_IN_USD_CENTS: u64 = 8;
|
||||
|
||||
/// Suno Sample via Artcraft is flat priced, so the cost state carries no
|
||||
/// request fields.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSunoSampleCostState;
|
||||
|
||||
impl ArtcraftSunoSampleCostState {
|
||||
pub fn from_request(_request: &ArtcraftSunoSampleRequestState) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
|
||||
AudioGenerationCostEstimate {
|
||||
cost_in_credits: Some(COST_IN_USD_CENTS),
|
||||
cost_in_usd_cents: Some(COST_IN_USD_CENTS),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn flat_cost_is_eight_cents() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoSample,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("mystical RPG adventure".to_string()),
|
||||
audio_references: Some(AudioListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_test123".to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
};
|
||||
let estimate = builder.build2().unwrap().estimate_cost().unwrap();
|
||||
assert_eq!(estimate.cost_in_usd_cents, Some(8));
|
||||
assert_eq!(estimate.cost_in_credits, Some(8));
|
||||
// Direct cost-state path matches the builder path.
|
||||
assert_eq!(ArtcraftSunoSampleCostState.estimate_cost().cost_in_usd_cents, Some(8));
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_audio_cost_and_generate_request::OmniGenAudioCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
use crate::generate::generate_audio::providers::artcraft::request_common::send_artcraft_omni_audio_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSunoSampleRequestState {
|
||||
pub request: OmniGenAudioCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSunoSampleRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_audio_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
|
||||
/// Replace with a real uploaded audio media token before running.
|
||||
/// (test_data doesn't have a production audio media token yet.)
|
||||
const AUDIO_MEDIA_TOKEN: &str = "m_replace_with_uploaded_audio_token";
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // sends a real generation to the Artcraft backend, incurs cost
|
||||
async fn sample_uploaded_audio() {
|
||||
let response = run_pipeline(GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoSample,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("Mystical RPG adventure, make it have a grand climax".to_string()),
|
||||
style_prompt: Some("Fantasy video game score".to_string()),
|
||||
is_instrumental: Some(true),
|
||||
audio_references: Some(AudioListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new(AUDIO_MEDIA_TOKEN.to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateAudioResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn get_artcraft_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
|
||||
.expect("Failed to read artcraft cookies");
|
||||
let cookies = cookies.trim().to_string();
|
||||
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
|
||||
.expect("Failed to parse cookies")
|
||||
.expect("No credentials found");
|
||||
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
|
||||
let client = get_artcraft_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
AudioGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Artcraft skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateAudioResponse::Artcraft(p) => {
|
||||
println!("inference_job_token={:?}", p.inference_job_token);
|
||||
println!("all_inference_job_tokens={:?}", p.all_inference_job_tokens);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_audio_model::CommonAudioModel as CommonAudioModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::audio_generation_request::AudioGenerationRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::providers::artcraft::build_common::build_artcraft_omni_audio_request;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_sounds::request::ArtcraftSunoSoundsRequestState;
|
||||
|
||||
pub fn build_artcraft_suno_sounds(builder: GenerateAudioRequestBuilder) -> Result<AudioGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_audio_request(builder, CommonAudioModelEnum::SunoSounds)?;
|
||||
let state = ArtcraftSunoSoundsRequestState { request };
|
||||
Ok(AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::ArtcraftSunoSounds(state)))
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
|
||||
use crate::generate::generate_audio::providers::artcraft::suno_sounds::request::ArtcraftSunoSoundsRequestState;
|
||||
|
||||
const COST_IN_USD_CENTS: u64 = 3;
|
||||
|
||||
/// Suno Sounds via Artcraft is flat priced, so the cost state carries no
|
||||
/// request fields.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSunoSoundsCostState;
|
||||
|
||||
impl ArtcraftSunoSoundsCostState {
|
||||
pub fn from_request(_request: &ArtcraftSunoSoundsRequestState) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
|
||||
AudioGenerationCostEstimate {
|
||||
cost_in_credits: Some(COST_IN_USD_CENTS),
|
||||
cost_in_usd_cents: Some(COST_IN_USD_CENTS),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn flat_cost_is_three_cents() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoSounds,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("rain sound effects".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let estimate = builder.build2().unwrap().estimate_cost().unwrap();
|
||||
assert_eq!(estimate.cost_in_usd_cents, Some(3));
|
||||
assert_eq!(estimate.cost_in_credits, Some(3));
|
||||
// Direct cost-state path matches the builder path.
|
||||
assert_eq!(ArtcraftSunoSoundsCostState.estimate_cost().cost_in_usd_cents, Some(3));
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_audio_cost_and_generate_request::OmniGenAudioCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
use crate::generate::generate_audio::providers::artcraft::request_common::send_artcraft_omni_audio_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSunoSoundsRequestState {
|
||||
pub request: OmniGenAudioCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSunoSoundsRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_audio_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use enums::common::generation::common_musical_key::CommonMusicalKey;
|
||||
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // sends a real generation to the Artcraft backend, incurs cost
|
||||
async fn loopable_sound_effect() {
|
||||
let response = run_pipeline(GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoSounds,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("Lo-fi vinyl crackle groove".to_string()),
|
||||
is_loopable: Some(true),
|
||||
bpm: Some(85),
|
||||
musical_key: Some(CommonMusicalKey::AMinor),
|
||||
..Default::default()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateAudioResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn get_artcraft_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
|
||||
.expect("Failed to read artcraft cookies");
|
||||
let cookies = cookies.trim().to_string();
|
||||
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
|
||||
.expect("Failed to parse cookies")
|
||||
.expect("No credentials found");
|
||||
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
|
||||
let client = get_artcraft_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
AudioGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Artcraft skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateAudioResponse::Artcraft(p) => {
|
||||
println!("inference_job_token={:?}", p.inference_job_token);
|
||||
println!("all_inference_job_tokens={:?}", p.all_inference_job_tokens);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod seed_audio_1p0;
|
||||
+588
@@ -0,0 +1,588 @@
|
||||
use fal_client::requests::api::audio::omni::seed_audio_1p0::api::{
|
||||
SeedAudio1p0OutputFormat, SeedAudio1p0Request, SeedAudio1p0SampleRate,
|
||||
};
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::audio_generation_request::AudioGenerationRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::providers::fal::seed_audio_1p0::request::FalSeedAudio1p0RequestState;
|
||||
use crate::generate::generate_audio::providers::reject_unsupported::reject_unsupported_option;
|
||||
|
||||
const MAX_AUDIO_REFERENCES: usize = 3;
|
||||
const MIN_SPEED: f64 = 0.5;
|
||||
const MAX_SPEED: f64 = 2.0;
|
||||
const MIN_VOLUME: f64 = 0.5;
|
||||
const MAX_VOLUME: f64 = 2.0;
|
||||
const MIN_PITCH: i8 = -12;
|
||||
const MAX_PITCH: i8 = 12;
|
||||
|
||||
pub fn build_fal_seed_audio_1p0(builder: GenerateAudioRequestBuilder) -> Result<AudioGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let state = build_fal_seed_audio_1p0_state(builder)?;
|
||||
Ok(AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::FalSeedAudio1p0(state)))
|
||||
}
|
||||
|
||||
pub(crate) fn build_fal_seed_audio_1p0_state(
|
||||
mut builder: GenerateAudioRequestBuilder,
|
||||
) -> Result<FalSeedAudio1p0RequestState, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Options Seed Audio has no equivalent for.
|
||||
reject_unsupported_option("style_prompt", builder.style_prompt.as_ref(), strategy)?;
|
||||
reject_unsupported_option("keep_lyrics", builder.keep_lyrics.as_ref(), strategy)?;
|
||||
reject_unsupported_option("is_instrumental", builder.is_instrumental.as_ref(), strategy)?;
|
||||
reject_unsupported_option("is_loopable", builder.is_loopable.as_ref(), strategy)?;
|
||||
reject_unsupported_option("bpm", builder.bpm.as_ref(), strategy)?;
|
||||
reject_unsupported_option("musical_key", builder.musical_key.as_ref(), strategy)?;
|
||||
|
||||
let prompt = builder.prompt.take().ok_or_else(|| {
|
||||
ArtcraftRouterError::InvalidInput("A prompt is required for Seed Audio 1.0".to_string())
|
||||
})?;
|
||||
|
||||
let audio_urls = plan_audio_urls(builder.audio_references.take(), strategy)?;
|
||||
let image_url = plan_image_url(builder.image_references.take(), strategy)?;
|
||||
|
||||
// fal rejects requests that combine an image reference with audio
|
||||
// references, so the router always rejects the combination too.
|
||||
if audio_urls.is_some() && image_url.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_references",
|
||||
value: "Seed Audio 1.0 cannot combine an image reference with audio references".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
let request = SeedAudio1p0Request {
|
||||
prompt,
|
||||
// The omni audio API doesn't expose voice selection yet, so the model
|
||||
// always picks the voice.
|
||||
voice: None,
|
||||
audio_urls,
|
||||
image_url,
|
||||
output_format: Some(SeedAudio1p0OutputFormat::Mp3),
|
||||
sample_rate: plan_sample_rate(builder.sample_rate_hz, strategy)?,
|
||||
speed: plan_multiplier("speed", builder.speed, MIN_SPEED, MAX_SPEED, strategy)?,
|
||||
volume: plan_multiplier("volume", builder.volume, MIN_VOLUME, MAX_VOLUME, strategy)?,
|
||||
pitch: plan_pitch(builder.pitch, strategy)?,
|
||||
};
|
||||
|
||||
Ok(FalSeedAudio1p0RequestState { request })
|
||||
}
|
||||
|
||||
// ── Plan helpers ──
|
||||
|
||||
/// Seed Audio takes up to 3 reference audio URLs. Fal only accepts URLs (not
|
||||
/// media tokens). More than 3 references reject under `ErrorOut`; the other
|
||||
/// strategies keep the first 3.
|
||||
fn plan_audio_urls(
|
||||
audio_references: Option<AudioListRef>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
|
||||
let mut urls = match audio_references {
|
||||
None => return Ok(None),
|
||||
Some(AudioListRef::Urls(urls)) => urls,
|
||||
Some(AudioListRef::MediaFileTokens(tokens)) if tokens.is_empty() => return Ok(None),
|
||||
Some(AudioListRef::MediaFileTokens(_)) => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls));
|
||||
}
|
||||
};
|
||||
if urls.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
if urls.len() > MAX_AUDIO_REFERENCES {
|
||||
match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "audio_references",
|
||||
value: format!(
|
||||
"Seed Audio 1.0 supports at most {} audio references, got {}",
|
||||
MAX_AUDIO_REFERENCES,
|
||||
urls.len(),
|
||||
),
|
||||
}));
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
urls.truncate(MAX_AUDIO_REFERENCES);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(urls))
|
||||
}
|
||||
|
||||
/// Seed Audio takes a single reference image URL. Fal only accepts URLs (not
|
||||
/// media tokens). More than 1 reference rejects under `ErrorOut`; the other
|
||||
/// strategies keep the first.
|
||||
fn plan_image_url(
|
||||
image_references: Option<ImageListRef>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
let urls = match image_references {
|
||||
None => return Ok(None),
|
||||
Some(ImageListRef::Urls(urls)) => urls,
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) if tokens.is_empty() => return Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls));
|
||||
}
|
||||
};
|
||||
if urls.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
if urls.len() > 1 {
|
||||
if let RequestMismatchMitigationStrategy::ErrorOut = strategy {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_references",
|
||||
value: format!("Seed Audio 1.0 supports at most 1 image reference, got {}", urls.len()),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(urls.into_iter().next())
|
||||
}
|
||||
|
||||
/// Map a requested sample rate to Seed Audio's supported rates. An exact
|
||||
/// match is required under `ErrorOut`; the other strategies pick the nearest
|
||||
/// supported rate (ties resolve to the lower rate).
|
||||
fn plan_sample_rate(
|
||||
sample_rate_hz: Option<u32>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<SeedAudio1p0SampleRate>, ArtcraftRouterError> {
|
||||
const SUPPORTED: [(u32, SeedAudio1p0SampleRate); 6] = [
|
||||
(8_000, SeedAudio1p0SampleRate::Hz8000),
|
||||
(16_000, SeedAudio1p0SampleRate::Hz16000),
|
||||
(24_000, SeedAudio1p0SampleRate::Hz24000),
|
||||
(32_000, SeedAudio1p0SampleRate::Hz32000),
|
||||
(44_100, SeedAudio1p0SampleRate::Hz44100),
|
||||
(48_000, SeedAudio1p0SampleRate::Hz48000),
|
||||
];
|
||||
|
||||
let Some(requested) = sample_rate_hz else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if let Some((_, rate)) = SUPPORTED.iter().find(|(hz, _)| *hz == requested) {
|
||||
return Ok(Some(*rate));
|
||||
}
|
||||
|
||||
match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "sample_rate_hz",
|
||||
value: format!("{}", requested),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
let (_, nearest) = SUPPORTED.iter()
|
||||
.min_by_key(|(hz, _)| requested.abs_diff(*hz))
|
||||
.expect("SUPPORTED is non-empty");
|
||||
Ok(Some(*nearest))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clamp a 0.5–2.0 multiplier (speed / volume) per strategy: out-of-range
|
||||
/// values reject under `ErrorOut` and clamp otherwise.
|
||||
fn plan_multiplier(
|
||||
field: &'static str,
|
||||
value: Option<f64>,
|
||||
min: f64,
|
||||
max: f64,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<f64>, ArtcraftRouterError> {
|
||||
let Some(value) = value else {
|
||||
return Ok(None);
|
||||
};
|
||||
if value >= min && value <= max {
|
||||
return Ok(Some(value));
|
||||
}
|
||||
match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field,
|
||||
value: format!("{}", value),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(value.clamp(min, max))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pitch is a semitone shift: round to the nearest whole semitone, then
|
||||
/// reject (under `ErrorOut`) or clamp values outside ±12.
|
||||
fn plan_pitch(
|
||||
pitch: Option<f64>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<i8>, ArtcraftRouterError> {
|
||||
let Some(pitch) = pitch else {
|
||||
return Ok(None);
|
||||
};
|
||||
let rounded = pitch.round();
|
||||
if rounded >= MIN_PITCH as f64 && rounded <= MAX_PITCH as f64 {
|
||||
return Ok(Some(rounded as i8));
|
||||
}
|
||||
match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "pitch",
|
||||
value: format!("{}", pitch),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(rounded.clamp(MIN_PITCH as f64, MAX_PITCH as f64) as i8))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
|
||||
use super::*;
|
||||
|
||||
const AUDIO_URL: &str = "https://example.com/a.mp3";
|
||||
const IMAGE_URL: &str = "https://example.com/scene.png";
|
||||
|
||||
mod basics {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
let state = build_fal_seed_audio_1p0_state(base_builder()).expect("build");
|
||||
assert_eq!(state.request.prompt, "a suspense radio drama");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prompt_is_rejected() {
|
||||
let builder = GenerateAudioRequestBuilder { prompt: None, ..base_builder() };
|
||||
assert!(build_fal_seed_audio_1p0_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn voice_is_always_none() {
|
||||
// The omni audio API doesn't expose voice selection.
|
||||
let state = build_fal_seed_audio_1p0_state(base_builder()).expect("build");
|
||||
assert!(state.request.voice.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_format_is_mp3() {
|
||||
let state = build_fal_seed_audio_1p0_state(base_builder()).expect("build");
|
||||
assert_eq!(state.request.output_format, Some(SeedAudio1p0OutputFormat::Mp3));
|
||||
}
|
||||
}
|
||||
|
||||
mod reference_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn audio_urls_pass_through() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
audio_references: Some(AudioListRef::Urls(vec![AUDIO_URL.to_string()])),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_seed_audio_1p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.audio_urls, Some(vec![AUDIO_URL.to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_audio_urls_are_accepted() {
|
||||
let builder = builder_with_audio_urls(3);
|
||||
let state = build_fal_seed_audio_1p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.audio_urls.as_ref().map(|u| u.len()), Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_audio_urls_error_out() {
|
||||
let mut builder = builder_with_audio_urls(4);
|
||||
builder.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::ErrorOut;
|
||||
assert!(build_fal_seed_audio_1p0_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_audio_urls_truncate_under_lenient_strategies() {
|
||||
let mut builder = builder_with_audio_urls(4);
|
||||
builder.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::PayMoreUpgrade;
|
||||
let state = build_fal_seed_audio_1p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.audio_urls.as_ref().map(|u| u.len()), Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_media_tokens_are_rejected() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
audio_references: Some(AudioListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_test123".to_string()),
|
||||
])),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_seed_audio_1p0_state(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_url_passes_through() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
image_references: Some(ImageListRef::Urls(vec![IMAGE_URL.to_string()])),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_seed_audio_1p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.image_url.as_deref(), Some(IMAGE_URL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_image_urls_error_out() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
image_references: Some(ImageListRef::Urls(vec![
|
||||
IMAGE_URL.to_string(),
|
||||
"https://example.com/other.png".to_string(),
|
||||
])),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_seed_audio_1p0_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combining_image_and_audio_references_is_always_rejected() {
|
||||
// fal rejects the combination, so the router rejects it under every
|
||||
// mitigation strategy.
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::ErrorOut,
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
audio_references: Some(AudioListRef::Urls(vec![AUDIO_URL.to_string()])),
|
||||
image_references: Some(ImageListRef::Urls(vec![IMAGE_URL.to_string()])),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(
|
||||
build_fal_seed_audio_1p0_state(builder).is_err(),
|
||||
"expected rejection under {strategy:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod sample_rate_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn exact_rates_map_directly() {
|
||||
let cases = [
|
||||
(8_000, SeedAudio1p0SampleRate::Hz8000),
|
||||
(16_000, SeedAudio1p0SampleRate::Hz16000),
|
||||
(24_000, SeedAudio1p0SampleRate::Hz24000),
|
||||
(32_000, SeedAudio1p0SampleRate::Hz32000),
|
||||
(44_100, SeedAudio1p0SampleRate::Hz44100),
|
||||
(48_000, SeedAudio1p0SampleRate::Hz48000),
|
||||
];
|
||||
for (hz, expected) in cases {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
sample_rate_hz: Some(hz),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_seed_audio_1p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.sample_rate, Some(expected), "for {hz} Hz");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inexact_rate_errors_out_under_error_out() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
sample_rate_hz: Some(22_050),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_seed_audio_1p0_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inexact_rate_maps_to_nearest_under_lenient_strategies() {
|
||||
let cases = [
|
||||
(7_000, SeedAudio1p0SampleRate::Hz8000),
|
||||
(22_050, SeedAudio1p0SampleRate::Hz24000),
|
||||
(40_000, SeedAudio1p0SampleRate::Hz44100),
|
||||
(96_000, SeedAudio1p0SampleRate::Hz48000),
|
||||
];
|
||||
for (hz, expected) in cases {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
sample_rate_hz: Some(hz),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_seed_audio_1p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.sample_rate, Some(expected), "for {hz} Hz");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_rate_stays_none() {
|
||||
let state = build_fal_seed_audio_1p0_state(base_builder()).expect("build");
|
||||
assert!(state.request.sample_rate.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod speed_and_volume_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn in_range_values_pass_through() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
speed: Some(1.5),
|
||||
volume: Some(0.5),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_seed_audio_1p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.speed, Some(1.5));
|
||||
assert_eq!(state.request.volume, Some(0.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_speed_errors_out() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
speed: Some(3.0),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_seed_audio_1p0_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_speed_clamps_under_lenient_strategies() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
speed: Some(3.0),
|
||||
volume: Some(0.1),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_seed_audio_1p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.speed, Some(2.0));
|
||||
assert_eq!(state.request.volume, Some(0.5));
|
||||
}
|
||||
}
|
||||
|
||||
mod pitch_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pitch_rounds_to_nearest_semitone() {
|
||||
let cases = [
|
||||
(2.4, 2i8),
|
||||
(2.5, 3i8),
|
||||
(-2.4, -2i8),
|
||||
(-2.5, -3i8),
|
||||
(0.0, 0i8),
|
||||
];
|
||||
for (input, expected) in cases {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
pitch: Some(input),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_seed_audio_1p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.pitch, Some(expected), "for pitch {input}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_pitch_errors_out() {
|
||||
for pitch in [13.0, -13.0] {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
pitch: Some(pitch),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_seed_audio_1p0_state(builder).is_err(), "for pitch {pitch}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_pitch_clamps_under_lenient_strategies() {
|
||||
let cases = [(20.0, 12i8), (-20.0, -12i8)];
|
||||
for (input, expected) in cases {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
pitch: Some(input),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_seed_audio_1p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.pitch, Some(expected), "for pitch {input}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_pitch_is_accepted() {
|
||||
for (input, expected) in [(12.0, 12i8), (-12.0, -12i8)] {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
pitch: Some(input),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_seed_audio_1p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.pitch, Some(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod unsupported_options {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_error_out() {
|
||||
use enums::common::generation::common_musical_key::CommonMusicalKey;
|
||||
let cases: Vec<GenerateAudioRequestBuilder> = vec![
|
||||
GenerateAudioRequestBuilder { style_prompt: Some("EDM".to_string()), ..base_builder() },
|
||||
GenerateAudioRequestBuilder { keep_lyrics: Some(true), ..base_builder() },
|
||||
GenerateAudioRequestBuilder { is_instrumental: Some(true), ..base_builder() },
|
||||
GenerateAudioRequestBuilder { is_loopable: Some(true), ..base_builder() },
|
||||
GenerateAudioRequestBuilder { bpm: Some(120), ..base_builder() },
|
||||
GenerateAudioRequestBuilder { musical_key: Some(CommonMusicalKey::CMajor), ..base_builder() },
|
||||
];
|
||||
for mut builder in cases {
|
||||
builder.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::ErrorOut;
|
||||
assert!(build_fal_seed_audio_1p0_state(builder).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_are_dropped_under_lenient_strategies() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
style_prompt: Some("EDM".to_string()),
|
||||
keep_lyrics: Some(true),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_seed_audio_1p0_state(builder).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SeedAudio1p0,
|
||||
provider: RouterProvider::Fal,
|
||||
prompt: Some("a suspense radio drama".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn builder_with_audio_urls(count: usize) -> GenerateAudioRequestBuilder {
|
||||
let urls = (0..count).map(|i| format!("https://example.com/a{i}.mp3")).collect();
|
||||
GenerateAudioRequestBuilder {
|
||||
audio_references: Some(AudioListRef::Urls(urls)),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
|
||||
use crate::generate::generate_audio::providers::fal::seed_audio_1p0::request::FalSeedAudio1p0RequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalSeedAudio1p0CostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalSeedAudio1p0CostState {
|
||||
pub fn from_request(request: &FalSeedAudio1p0RequestState) -> Self {
|
||||
// Cost math is owned by fal_client's per-endpoint
|
||||
// `FalRequestCostCalculator` implementations. The router state just
|
||||
// forwards the result so router cost ≡ fal_client cost by construction.
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
|
||||
AudioGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::providers::fal::seed_audio_1p0::build::build_fal_seed_audio_1p0_state;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn estimate_is_nineteen_cents() {
|
||||
let state = build_fal_seed_audio_1p0_state(base_builder()).expect("build");
|
||||
let estimate = FalSeedAudio1p0CostState::from_request(&state).estimate_cost();
|
||||
assert_eq!(estimate.cost_in_usd_cents, Some(19));
|
||||
assert_eq!(estimate.cost_in_credits, Some(19));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_do_not_change_the_estimate() {
|
||||
let tuned = GenerateAudioRequestBuilder {
|
||||
sample_rate_hz: Some(48_000),
|
||||
speed: Some(2.0),
|
||||
volume: Some(0.5),
|
||||
pitch: Some(-12.0),
|
||||
..base_builder()
|
||||
};
|
||||
let base_state = build_fal_seed_audio_1p0_state(base_builder()).expect("build");
|
||||
let tuned_state = build_fal_seed_audio_1p0_state(tuned).expect("build");
|
||||
assert_eq!(
|
||||
FalSeedAudio1p0CostState::from_request(&base_state).cost_in_usd_cents,
|
||||
FalSeedAudio1p0CostState::from_request(&tuned_state).cost_in_usd_cents,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SeedAudio1p0,
|
||||
provider: RouterProvider::Fal,
|
||||
prompt: Some("a calm narrator describes ocean waves".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::api::audio::omni::seed_audio_1p0::api::SeedAudio1p0Request;
|
||||
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::generate_audio_response::{
|
||||
FalAudioResponsePayload, GenerateAudioResponse,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalSeedAudio1p0RequestState {
|
||||
/// Final materialized request; ready to fire.
|
||||
pub request: SeedAudio1p0Request,
|
||||
}
|
||||
|
||||
impl FalSeedAudio1p0RequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(self.request.clone());
|
||||
|
||||
let payload = if let Some(webhook_url) = &client.webhook_url {
|
||||
let response = self.request.send_webhook_request(&client.api_key, webhook_url).await?;
|
||||
FalAudioResponsePayload {
|
||||
request_id: response.request_id,
|
||||
gateway_request_id: response.gateway_request_id,
|
||||
maybe_status_url: None,
|
||||
maybe_response_url: None,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}
|
||||
} else {
|
||||
let response = self.request.send_queue_request(&client.api_key).await?;
|
||||
FalAudioResponsePayload {
|
||||
request_id: Some(response.request_id),
|
||||
gateway_request_id: None,
|
||||
maybe_status_url: Some(response.status_url),
|
||||
maybe_response_url: Some(response.response_url),
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(GenerateAudioResponse::Fal(payload))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_seed_audio_webhook() {
|
||||
let response = run_pipeline(GenerateAudioRequestBuilder {
|
||||
prompt: Some("A calm narrator describes ocean waves rolling onto a moonlit beach.".to_string()),
|
||||
..seed_audio_builder()
|
||||
}).await;
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_seed_audio_tuned() {
|
||||
let response = run_pipeline(GenerateAudioRequestBuilder {
|
||||
prompt: Some("Welcome aboard the midnight express. Please keep your arms inside the train.".to_string()),
|
||||
sample_rate_hz: Some(44_100),
|
||||
speed: Some(0.9),
|
||||
volume: Some(1.2),
|
||||
pitch: Some(-2.0),
|
||||
..seed_audio_builder()
|
||||
}).await;
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn seed_audio_builder() -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SeedAudio1p0,
|
||||
provider: RouterProvider::Fal,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_fal_client() -> RouterClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
let api_key = FalApiKey::from_str(secret.trim());
|
||||
let webhook_url = "https://example.com/fal-webhook-test".to_string();
|
||||
RouterClient::Fal(RouterFalClient::new_with_webhook(api_key, webhook_url))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
|
||||
let client = get_fal_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
AudioGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Seed Audio skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
println!("response: {:?}", response);
|
||||
response
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
pub mod suno_music;
|
||||
pub mod suno_remix;
|
||||
pub mod suno_sample;
|
||||
pub mod suno_sounds;
|
||||
pub(crate) mod resolve;
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
//! Shared resolve helpers for the Kinovi audio provider.
|
||||
//!
|
||||
//! The remix/sample models take exactly one user audio reference, which must
|
||||
//! be downloaded from our CDN and re-uploaded to the Kinovi CDN during the
|
||||
//! draft `finalize()` phase. The upload plumbing is shared with the Kinovi
|
||||
//! video provider (`generate_video::providers::kinovi::upload`).
|
||||
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
|
||||
use crate::generate::generate_video::providers::kinovi::upload::upload_to_seedance2pro;
|
||||
|
||||
/// A single unresolved audio reference: either an ArtCraft/public URL or a
|
||||
/// media file token that resolves to one via the draft context's map.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SingleAudioRef {
|
||||
Url(String),
|
||||
MediaFileToken(MediaFileToken),
|
||||
}
|
||||
|
||||
/// Extract exactly one audio reference from the builder's audio list.
|
||||
/// The Kinovi remix/sample models operate on a single source track, so zero
|
||||
/// or multiple references are rejected regardless of mitigation strategy.
|
||||
pub(crate) fn require_single_audio_ref(
|
||||
audio_references: Option<AudioListRef>,
|
||||
) -> Result<SingleAudioRef, ArtcraftRouterError> {
|
||||
let refs = match audio_references {
|
||||
None => {
|
||||
return Err(ArtcraftRouterError::InvalidInput(
|
||||
"Exactly one audio reference is required, but none was provided".to_string(),
|
||||
));
|
||||
}
|
||||
Some(AudioListRef::Urls(urls)) => urls.into_iter().map(SingleAudioRef::Url).collect::<Vec<_>>(),
|
||||
Some(AudioListRef::MediaFileTokens(tokens)) => {
|
||||
tokens.into_iter().map(SingleAudioRef::MediaFileToken).collect::<Vec<_>>()
|
||||
}
|
||||
};
|
||||
|
||||
match refs.len() {
|
||||
0 => Err(ArtcraftRouterError::InvalidInput(
|
||||
"Exactly one audio reference is required, but none was provided".to_string(),
|
||||
)),
|
||||
1 => Ok(refs.into_iter().next().expect("length checked above")),
|
||||
count => Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "audio_references",
|
||||
value: format!("Exactly one audio reference is required, got {}", count),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a single audio reference to a source URL and re-upload it to the
|
||||
/// Kinovi CDN, returning the Kinovi CDN URL.
|
||||
pub(crate) async fn resolve_and_upload_audio_ref(
|
||||
session: &Seedance2ProSession,
|
||||
audio_ref: &SingleAudioRef,
|
||||
draft_context: &AudioGenerationDraftContext<'_>,
|
||||
) -> Result<String, ArtcraftRouterError> {
|
||||
let source_url = match audio_ref {
|
||||
SingleAudioRef::Url(url) => url.clone(),
|
||||
SingleAudioRef::MediaFileToken(token) => {
|
||||
let map = draft_context.media_file_to_artcraft_url_map
|
||||
.ok_or(ArtcraftRouterError::Client(ClientError::MediaFileToUrlMapNotProvided))?;
|
||||
map.get(token).cloned().ok_or_else(|| {
|
||||
ArtcraftRouterError::Client(ClientError::MediaFileTokenNotFoundInMap {
|
||||
token: token.clone(),
|
||||
})
|
||||
})?
|
||||
}
|
||||
};
|
||||
upload_to_seedance2pro(session, &source_url).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod require_single_audio_ref_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn none_is_rejected() {
|
||||
assert!(require_single_audio_ref(None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_url_list_is_rejected() {
|
||||
assert!(require_single_audio_ref(Some(AudioListRef::Urls(vec![]))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_token_list_is_rejected() {
|
||||
assert!(require_single_audio_ref(Some(AudioListRef::MediaFileTokens(vec![]))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_url_is_accepted() {
|
||||
let result = require_single_audio_ref(Some(AudioListRef::Urls(vec![
|
||||
"https://example.com/a.mp3".to_string(),
|
||||
]))).expect("should accept a single URL");
|
||||
assert!(matches!(result, SingleAudioRef::Url(url) if url == "https://example.com/a.mp3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_token_is_accepted() {
|
||||
let result = require_single_audio_ref(Some(AudioListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_test123".to_string()),
|
||||
]))).expect("should accept a single token");
|
||||
assert!(matches!(result, SingleAudioRef::MediaFileToken(t) if t.as_str() == "mf_test123"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_urls_are_rejected() {
|
||||
let result = require_single_audio_ref(Some(AudioListRef::Urls(vec![
|
||||
"https://example.com/a.mp3".to_string(),
|
||||
"https://example.com/b.mp3".to_string(),
|
||||
])));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_tokens_are_rejected() {
|
||||
let result = require_single_audio_ref(Some(AudioListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_a".to_string()),
|
||||
MediaFileToken::new("mf_b".to_string()),
|
||||
])));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
use seedance2pro_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::audio_generation_request::AudioGenerationRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_music::request::KinoviSunoMusicRequestState;
|
||||
use crate::generate::generate_audio::providers::reject_unsupported::{
|
||||
reject_unsupported_audio_references, reject_unsupported_image_references,
|
||||
reject_unsupported_option,
|
||||
};
|
||||
|
||||
pub fn build_kinovi_suno_music(builder: GenerateAudioRequestBuilder) -> Result<AudioGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let state = build_kinovi_suno_music_state(builder)?;
|
||||
Ok(AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::KinoviSunoMusic(state)))
|
||||
}
|
||||
|
||||
pub(crate) fn build_kinovi_suno_music_state(
|
||||
mut builder: GenerateAudioRequestBuilder,
|
||||
) -> Result<KinoviSunoMusicRequestState, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Options Suno Music has no equivalent for.
|
||||
reject_unsupported_option("keep_lyrics", builder.keep_lyrics.as_ref(), strategy)?;
|
||||
reject_unsupported_option("is_loopable", builder.is_loopable.as_ref(), strategy)?;
|
||||
reject_unsupported_option("bpm", builder.bpm.as_ref(), strategy)?;
|
||||
reject_unsupported_option("musical_key", builder.musical_key.as_ref(), strategy)?;
|
||||
reject_unsupported_option("sample_rate_hz", builder.sample_rate_hz.as_ref(), strategy)?;
|
||||
reject_unsupported_option("speed", builder.speed.as_ref(), strategy)?;
|
||||
reject_unsupported_option("volume", builder.volume.as_ref(), strategy)?;
|
||||
reject_unsupported_option("pitch", builder.pitch.as_ref(), strategy)?;
|
||||
reject_unsupported_audio_references(builder.audio_references.as_ref(), strategy)?;
|
||||
reject_unsupported_image_references(builder.image_references.as_ref(), strategy)?;
|
||||
|
||||
let prompt = builder.prompt.take().ok_or_else(|| {
|
||||
ArtcraftRouterError::InvalidInput("A prompt is required for Suno Music".to_string())
|
||||
})?;
|
||||
|
||||
let request = GenerateSunoMusicRequest {
|
||||
prompt,
|
||||
style_tags: builder.style_prompt.take(),
|
||||
instrumental: builder.is_instrumental.unwrap_or(false),
|
||||
};
|
||||
|
||||
Ok(KinoviSunoMusicRequestState { request })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prompt_and_style_are_passed_through() {
|
||||
let state = build_kinovi_suno_music_state(base_builder()).expect("build");
|
||||
assert_eq!(state.request.prompt, "a song about corgis");
|
||||
assert_eq!(state.request.style_tags.as_deref(), Some("sea shanty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prompt_is_rejected() {
|
||||
let builder = GenerateAudioRequestBuilder { prompt: None, ..base_builder() };
|
||||
assert!(build_kinovi_suno_music_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instrumental_defaults_to_false() {
|
||||
let state = build_kinovi_suno_music_state(base_builder()).expect("build");
|
||||
assert!(!state.request.instrumental);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instrumental_passes_through() {
|
||||
let builder = GenerateAudioRequestBuilder { is_instrumental: Some(true), ..base_builder() };
|
||||
let state = build_kinovi_suno_music_state(builder).expect("build");
|
||||
assert!(state.request.instrumental);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_error_out() {
|
||||
let error_out = RequestMismatchMitigationStrategy::ErrorOut;
|
||||
let cases: Vec<GenerateAudioRequestBuilder> = vec![
|
||||
GenerateAudioRequestBuilder { keep_lyrics: Some(true), ..base_builder() },
|
||||
GenerateAudioRequestBuilder { is_loopable: Some(true), ..base_builder() },
|
||||
GenerateAudioRequestBuilder { bpm: Some(120), ..base_builder() },
|
||||
GenerateAudioRequestBuilder { sample_rate_hz: Some(48_000), ..base_builder() },
|
||||
GenerateAudioRequestBuilder { speed: Some(1.5), ..base_builder() },
|
||||
GenerateAudioRequestBuilder { volume: Some(0.5), ..base_builder() },
|
||||
GenerateAudioRequestBuilder { pitch: Some(2.0), ..base_builder() },
|
||||
GenerateAudioRequestBuilder {
|
||||
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.mp3".to_string()])),
|
||||
..base_builder()
|
||||
},
|
||||
];
|
||||
for mut builder in cases {
|
||||
builder.request_mismatch_mitigation_strategy = error_out;
|
||||
assert!(build_kinovi_suno_music_state(builder).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_are_dropped_under_lenient_strategies() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
keep_lyrics: Some(true),
|
||||
bpm: Some(120),
|
||||
speed: Some(1.5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_kinovi_suno_music_state(builder).is_ok());
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoMusic,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("a song about corgis".to_string()),
|
||||
style_prompt: Some("sea shanty".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
use seedance2pro_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
|
||||
|
||||
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_music::request::KinoviSunoMusicRequestState;
|
||||
|
||||
/// Suno Music is flat priced, so the cost state carries no request fields.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KinoviSunoMusicCostState;
|
||||
|
||||
impl KinoviSunoMusicCostState {
|
||||
pub fn from_request(_request: &KinoviSunoMusicRequestState) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
|
||||
// Cost math is owned by seedance2pro_client's binding — the router just
|
||||
// forwards the result so router cost ≡ binding cost by construction.
|
||||
let pricing_request = GenerateSunoMusicRequest {
|
||||
prompt: String::new(),
|
||||
style_tags: None,
|
||||
instrumental: false,
|
||||
};
|
||||
let costs = pricing_request.calculate_costs();
|
||||
|
||||
AudioGenerationCostEstimate {
|
||||
cost_in_credits: Some(costs.kinovi_credits),
|
||||
cost_in_usd_cents: Some(costs.usd_cents_rounded_up),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::providers::kinovi::suno_music::build::build_kinovi_suno_music_state;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn flat_cost_is_seven_cents() {
|
||||
let estimate = KinoviSunoMusicCostState.estimate_cost();
|
||||
assert_eq!(estimate.cost_in_usd_cents, Some(7));
|
||||
assert_eq!(estimate.cost_in_credits, Some(16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_from_built_request_is_seven_cents() {
|
||||
let builder = GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoMusic,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("a song".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let state = build_kinovi_suno_music_state(builder).expect("build");
|
||||
let estimate = KinoviSunoMusicCostState::from_request(&state).estimate_cost();
|
||||
assert_eq!(estimate.cost_in_usd_cents, Some(7));
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
use seedance2pro_client::generate::audio::generate_suno_music::{
|
||||
generate_suno_music, GenerateSunoMusicArgs, GenerateSunoMusicRequest,
|
||||
};
|
||||
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_audio::generate_audio_response::{
|
||||
GenerateAudioResponse, Seedance2proAudioResponsePayload,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KinoviSunoMusicRequestState {
|
||||
/// Final materialized request; ready to fire.
|
||||
pub request: GenerateSunoMusicRequest,
|
||||
}
|
||||
|
||||
impl KinoviSunoMusicRequestState {
|
||||
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
|
||||
let session = &client.session;
|
||||
|
||||
let args = GenerateSunoMusicArgs {
|
||||
session,
|
||||
host_override: None,
|
||||
request: self.request.clone(),
|
||||
};
|
||||
|
||||
let response = generate_suno_music(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(GenerateAudioResponse::Seedance2Pro(Seedance2proAudioResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
|
||||
use crate::api::router_audio_model::RouterAudioModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
|
||||
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
|
||||
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Sends a real generation to Kinovi; costs credits.
|
||||
async fn music_with_vocals() {
|
||||
let response = run_pipeline(GenerateAudioRequestBuilder {
|
||||
prompt: Some("A song about a corgi who learns to sail the open sea".to_string()),
|
||||
style_prompt: Some("Sea shanty, folk".to_string()),
|
||||
..suno_music_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Sends a real generation to Kinovi; costs credits.
|
||||
async fn music_instrumental() {
|
||||
let response = run_pipeline(GenerateAudioRequestBuilder {
|
||||
prompt: Some("An epic journey across a frozen mountain range".to_string()),
|
||||
style_prompt: Some("Cinematic orchestral score".to_string()),
|
||||
is_instrumental: Some(true),
|
||||
..suno_music_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn suno_music_builder() -> GenerateAudioRequestBuilder {
|
||||
GenerateAudioRequestBuilder {
|
||||
model: RouterAudioModel::SunoMusic,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_seedance2pro_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
|
||||
.expect("Failed to read seedance2pro cookies");
|
||||
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
|
||||
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
|
||||
let client = get_seedance2pro_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
AudioGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Suno Music skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateAudioResponse::Seedance2Pro(p) => {
|
||||
println!("task_id={}, order_id={}", p.task_id, p.order_id);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user