mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-25 03:09:30 -04:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38cbd724d4 | ||
|
|
f669e0071e | ||
|
|
a8af0e8141 | ||
|
|
a78ac8c391 | ||
|
|
3a62afb53c | ||
|
|
998a7b60b1 | ||
|
|
aeebad3a40 | ||
|
|
bb53238c56 |
-179
@@ -1,179 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at,\n (\n f.maybe_parent_folder_token IS NOT NULL\n AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)\n ) as `is_orphaned: bool`\nFROM folders f\nLEFT JOIN folders p\n ON p.token = f.maybe_parent_folder_token\nWHERE f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\nORDER BY f.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": "token: FolderToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "owner_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_parent_folder_token: FolderToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_last_media_file_token_1: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_last_media_file_token_2: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_last_media_file_token_3: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_last_media_file_token_4: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_color_code",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "has_star: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "is_orphaned: bool",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "05a880a2ece0236f57fe1c40100cba0c0cdcad2ebee0fecb88f9043ac7b82d8c"
|
||||
}
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at,\n (\n f.maybe_parent_folder_token IS NOT NULL\n AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)\n ) as `is_orphaned: bool`\nFROM folders f\nLEFT JOIN folders p\n ON p.token = f.maybe_parent_folder_token\nWHERE f.token = ?\n AND f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\nLIMIT 1\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": "token: FolderToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "owner_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_parent_folder_token: FolderToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_last_media_file_token_1: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_last_media_file_token_2: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_last_media_file_token_3: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_last_media_file_token_4: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_color_code",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "has_star: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "is_orphaned: bool",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0c918aacf4ce34e1e03d2f9af49dd3138cadf59a28d0917726135ed5f34cba5e"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO user_email_changes\nSET\n user_token = ?,\n old_email = ?,\n new_email = ?,\n ip_address = ?,\n maybe_changed_by_user_token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1359fb54af12d78251fe435b063771984ee00d74a71b5d29e9df267991f38522"
|
||||
}
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = ?,\n maybe_model_token = ?,\n\n maybe_input_source_token = ?,\n maybe_input_source_token_type = ?,\n\n maybe_download_url = ?,\n maybe_cover_image_media_file_token = ?,\n\n maybe_prompt_token = ?,\n maybe_wallet_ledger_entry_token = ?,\n\n maybe_raw_inference_text = ?,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n platform_type = ?,\n\n priority_level = ?,\n is_keepalive_required = ?,\n max_duration_seconds = ?,\n is_debug_request = ?,\n\n maybe_routing_tag = ?,\n\n maybe_debug_log_event_token = ?,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = ?,\n maybe_model_token = ?,\n\n maybe_input_source_token = ?,\n maybe_input_source_token_type = ?,\n\n maybe_download_url = ?,\n maybe_cover_image_media_file_token = ?,\n\n maybe_prompt_token = ?,\n maybe_wallet_ledger_entry_token = ?,\n\n maybe_raw_inference_text = ?,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = ?,\n is_keepalive_required = ?,\n max_duration_seconds = ?,\n is_debug_request = ?,\n\n maybe_routing_tag = ?,\n\n maybe_debug_log_event_token = ?,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 31
|
||||
"Right": 30
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "108731dc51d915ebb8b6ed391c5bae4c41ae6f2c813be92c07d8a10ca401db53"
|
||||
"hash": "161febbb30cf6c669a813fe036d405ef428422297440abd240482a6aae51f6f9"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE folders\nSET maybe_cover_image_custom_media_token = ?\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "197e4fb3822b095cbbf8127493dad91c9254b7c242b8921a8c2df9f5a0e77424"
|
||||
}
|
||||
+2
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `request_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `request_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -82,16 +82,6 @@
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -105,9 +95,8 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1a0b60602debe525529f50566fc545f1cb4b77005f423a10464652be7fbfa839"
|
||||
"hash": "1ddb2207db50e2dbfb37d26c006bffe2d8f5fb011855d5a86d01f395c36df759"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE folders\nSET has_star = ?\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "24716cfdb77eee365bd605a40cecb02b3a27a90725dbe96e3865545d14839ab8"
|
||||
}
|
||||
+3
-14
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n \n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n \n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -95,16 +95,6 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -114,7 +104,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 10,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -137,10 +127,9 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3a846909a725eb776b4a4637dc64dcaf1ed70ad8891f9d012c7f3cdcb16a613e"
|
||||
"hash": "2c98621c08055374fd09502c94ac40bb01d23fcea3f844cceea7446a5c0cb4dc"
|
||||
}
|
||||
+3
-14
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n\n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n\n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -95,16 +95,6 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -114,7 +104,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 10,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -137,10 +127,9 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "38a1d7b676bb0987fc191bbc8bdeadf6174a19358dff40d9772b9d5824145bbc"
|
||||
"hash": "31d85cc0013f29e6db2d272ee6e709cca79b46496a2c222960f1a1419175f00b"
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT DISTINCT\n subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n maybe_stripe_customer_id as `stripe_customer_id!`\n\nFROM user_subscriptions\n\nWHERE\n user_token = ?\n AND maybe_stripe_customer_id IS NOT NULL\n AND deleted_at IS NULL\nORDER BY subscription_namespace ASC, maybe_stripe_customer_id ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "subscription_namespace: enums::common::payments_namespace::PaymentsNamespace",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "stripe_customer_id!",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "516db34b6a642a2f7d7107f186905a79ad694c3c86f3078efc77bcaae7f39f3d"
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n maybe_creator_user_token as `maybe_creator_user_token: UserToken`,\n media_class as `media_class: MediaFileClass`,\n media_type as `media_type: MediaFileType`,\n maybe_cover_image_media_file_token as `maybe_cover_image_media_file_token: MediaFileToken`\nFROM media_files\nWHERE token = ?\n AND user_deleted_at IS NULL\n AND mod_deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "maybe_creator_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "media_class: MediaFileClass",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "media_type: MediaFileType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_cover_image_media_file_token: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5606a694c872d868ab3b35f4b3685f86f2b27ad16e741c8b1bf997b613f1b803"
|
||||
}
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n uec.id as `id: u64`,\n uec.old_email,\n uec.new_email,\n uec.ip_address,\n uec.created_at,\n\n u_subject.token as `user_token: tokens::tokens::users::UserToken`,\n u_subject.username as user_username,\n u_subject.display_name as user_display_name,\n u_subject.email_gravatar_hash as user_gravatar_hash,\n\n u_changer.token as `maybe_changed_by_user_token?: tokens::tokens::users::UserToken`,\n u_changer.username as `maybe_changed_by_user_username?`,\n u_changer.display_name as `maybe_changed_by_user_display_name?`,\n u_changer.email_gravatar_hash as `maybe_changed_by_user_gravatar_hash?`\n\nFROM user_email_changes AS uec\nJOIN users AS u_subject\n ON uec.user_token = u_subject.token\nLEFT JOIN users AS u_changer\n ON uec.maybe_changed_by_user_token = u_changer.token\nWHERE uec.user_token = ?\n AND uec.id < ?\nORDER BY uec.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": "old_email",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "new_email",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 160
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "user_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "user_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "user_gravatar_hash",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_changed_by_user_token?: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_changed_by_user_username?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_changed_by_user_display_name?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "maybe_changed_by_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,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5611678b6c168208ea4da9ef0e3071758c9bb8d89b54e2d3f0fc84bd0a6c915f"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE folders\nSET maybe_color_code = ?\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6a0e3f012795068a56a0dfaf567d066dc157bb06c736c1c4314b18c8e0356127"
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nWITH RECURSIVE ancestor_chain (token, parent_token, depth) AS (\n SELECT token, maybe_parent_folder_token, 0\n FROM folders\n WHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\n UNION ALL\n SELECT f.token, f.maybe_parent_folder_token, ac.depth + 1\n FROM folders f\n INNER JOIN ancestor_chain ac\n ON f.token = ac.parent_token\n WHERE f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\n AND ac.depth < ?\n)\nSELECT token as `token!: FolderToken`\nFROM ancestor_chain\nWHERE depth > 0\nORDER BY depth ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token!: FolderToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "780891e8294b78cc0e5a05a8b5a8949a763bcb8c5d6526a621234020b154bdc9"
|
||||
}
|
||||
-267
@@ -1,267 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\nORDER BY mf.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "media_file_id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "added_to_folder_at: DateTime<Utc>",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "media_file_token: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "media_class: MediaFileClass",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "media_type: MediaFileType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_batch_token: BatchGenerationToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "public_bucket_directory_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_cover_public_bucket_directory_hash?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_cover_public_bucket_prefix?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_cover_public_bucket_extension?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "creator_set_visibility: Visibility",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | BINARY | ENUM",
|
||||
"char_set": 224,
|
||||
"max_size": 28
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "is_user_upload: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "is_intermediate_system_file: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "maybe_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "maybe_prompt_token: PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "maybe_origin_filename",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "maybe_duration_millis",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "maybe_frame_width",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "maybe_frame_height",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "8aca9bc976b980ea1a3b3052bc8a45dd4d8db55c20fd1aede2a4415cc1395cf3"
|
||||
}
|
||||
+2
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.job_type = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.job_type = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -82,16 +82,6 @@
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -105,9 +95,8 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ddbbd3886c00268edd3993508f635549fc8ddc877bb059fd58f14b8c07586fc9"
|
||||
"hash": "a0002183d4d077b07bb0cd062ca95bf113d1564ce3195f0e62e531ce4297dd76"
|
||||
}
|
||||
-168
@@ -1,168 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at\nFROM folders f\nWHERE f.maybe_parent_folder_token = ?\n AND f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\nORDER BY f.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": "token: FolderToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "owner_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_parent_folder_token: FolderToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_last_media_file_token_1: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_last_media_file_token_2: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_last_media_file_token_3: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_last_media_file_token_4: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_color_code",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "has_star: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a13680fb2b54809b6d9d9356f8eec87cd0f1f37405b84bbf734ed06cc244efb1"
|
||||
}
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_generation_provider = ?,\n\n platform_type = ?,\n\n maybe_cover_image_media_file_token = ?,\n\n maybe_mod_user_token = ?,\n is_generated_on_prem = ?,\n generated_by_worker = ?,\n generated_by_cluster = ?\n ",
|
||||
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_generation_provider = ?,\n\n maybe_cover_image_media_file_token = ?,\n\n maybe_mod_user_token = ?,\n is_generated_on_prem = ?,\n generated_by_worker = ?,\n generated_by_cluster = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 42
|
||||
"Right": 41
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ad2ffbd9f48e52678a5c61817bec043bf1df4033463e5c5a18468c04af391e6a"
|
||||
"hash": "a4901f904aa1c00d9afc23faa7008816c6e1b653bd6ad9f0b6772e32000be544"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE folders\nSET\n maybe_last_media_file_token_1 = (\n SELECT mf.token\n FROM folder_media_files fmf\n JOIN media_files mf\n ON mf.token = fmf.media_file_token\n WHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n ORDER BY fmf.id DESC\n LIMIT 1 OFFSET 0\n ),\n maybe_last_media_file_token_2 = (\n SELECT mf.token\n FROM folder_media_files fmf\n JOIN media_files mf\n ON mf.token = fmf.media_file_token\n WHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n ORDER BY fmf.id DESC\n LIMIT 1 OFFSET 1\n ),\n maybe_last_media_file_token_3 = (\n SELECT mf.token\n FROM folder_media_files fmf\n JOIN media_files mf\n ON mf.token = fmf.media_file_token\n WHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n ORDER BY fmf.id DESC\n LIMIT 1 OFFSET 2\n ),\n maybe_last_media_file_token_4 = (\n SELECT mf.token\n FROM folder_media_files fmf\n JOIN media_files mf\n ON mf.token = fmf.media_file_token\n WHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n ORDER BY fmf.id DESC\n LIMIT 1 OFFSET 3\n )\nWHERE token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ae417955113b385031181772b29f234ab9297cf583583f74fe287bf9c24b1e6a"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO folders\nSET\n token = ?,\n name = ?,\n owner_user_token = ?,\n maybe_parent_folder_token = ?,\n maybe_color_code = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c099b274026619760f0982f0e5082da1e8de16c26122f2750667b9cdc2c4abf3"
|
||||
}
|
||||
-267
@@ -1,267 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "media_file_id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "added_to_folder_at: DateTime<Utc>",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "media_file_token: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "media_class: MediaFileClass",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "media_type: MediaFileType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_batch_token: BatchGenerationToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "public_bucket_directory_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_cover_public_bucket_directory_hash?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_cover_public_bucket_prefix?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_cover_public_bucket_extension?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "creator_set_visibility: Visibility",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | BINARY | ENUM",
|
||||
"char_set": 224,
|
||||
"max_size": 28
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "is_user_upload: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "is_intermediate_system_file: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "maybe_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "maybe_prompt_token: PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "maybe_origin_filename",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "maybe_duration_millis",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "maybe_frame_width",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "maybe_frame_height",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c0ba9f9aa3184b1cb3d714173b31e530e201a6c24afd29ad534cf7f3954c7316"
|
||||
}
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n uec.id as `id: u64`,\n uec.old_email,\n uec.new_email,\n uec.ip_address,\n uec.created_at,\n\n u_subject.token as `user_token: tokens::tokens::users::UserToken`,\n u_subject.username as user_username,\n u_subject.display_name as user_display_name,\n u_subject.email_gravatar_hash as user_gravatar_hash,\n\n u_changer.token as `maybe_changed_by_user_token?: tokens::tokens::users::UserToken`,\n u_changer.username as `maybe_changed_by_user_username?`,\n u_changer.display_name as `maybe_changed_by_user_display_name?`,\n u_changer.email_gravatar_hash as `maybe_changed_by_user_gravatar_hash?`\n\nFROM user_email_changes AS uec\nJOIN users AS u_subject\n ON uec.user_token = u_subject.token\nLEFT JOIN users AS u_changer\n ON uec.maybe_changed_by_user_token = u_changer.token\nWHERE uec.user_token = ?\nORDER BY uec.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": "old_email",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "new_email",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 160
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "user_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "user_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "user_gravatar_hash",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_changed_by_user_token?: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_changed_by_user_username?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_changed_by_user_display_name?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "maybe_changed_by_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,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c17e1d6b567dd378bb4cb0478fb9558e907a0f36498bd049c7c526e496e89259"
|
||||
}
|
||||
+2
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `operation_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `operation_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -82,16 +82,6 @@
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -105,9 +95,8 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5f24aeb12e5a78ffedd1c772c594b668188aaaf01f315a1c41c75610fdfc1b32"
|
||||
"hash": "d56a26a7300495fac9741e77e390f16402e2273dbab9a15d7c17eb1f0da7a1b1"
|
||||
}
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at,\n (\n f.maybe_parent_folder_token IS NOT NULL\n AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)\n ) as `is_orphaned: bool`\nFROM folders f\nLEFT JOIN folders p\n ON p.token = f.maybe_parent_folder_token\nWHERE f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\n AND f.id < ?\nORDER BY f.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": "token: FolderToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "owner_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_parent_folder_token: FolderToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_last_media_file_token_1: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_last_media_file_token_2: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_last_media_file_token_3: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_last_media_file_token_4: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_color_code",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "has_star: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "is_orphaned: bool",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d6f0c65abce2da20fe86805ff053b8c2569bf0095f6245a5529f0c71854a604b"
|
||||
}
|
||||
-168
@@ -1,168 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at\nFROM folders f\nWHERE f.maybe_parent_folder_token = ?\n AND f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\n AND f.id < ?\nORDER BY f.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": "token: FolderToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "owner_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_parent_folder_token: FolderToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_last_media_file_token_1: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_last_media_file_token_2: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_last_media_file_token_3: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_last_media_file_token_4: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_color_code",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "has_star: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d85c1a844f14b9416c6260d7a906fec709b50eb4583b98dffb52ebbe6abcb7d5"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE folders\nSET name = ?\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e32e195ef41bc833e5b7fc53cfde8669f88aeb66d279e5213d047f4860ec86ab"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE folders\nSET maybe_deleted_at = NOW()\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f9e3ff8ca682948076b799d6b2d934be4a87a4e04665d6f12cc32095494129ad"
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n user_token as `user_token: tokens::tokens::users::UserToken`,\n payments_namespace as `payments_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n stripe_customer_id\n\nFROM user_stripe_customer_links\n\nWHERE user_token = ?\nORDER BY id ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "payments_namespace: enums::common::payments_namespace::PaymentsNamespace",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "stripe_customer_id",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ffb5717340f21f21693bfcade201bc055fb3fb8a26da8d0574e3de44b033586c"
|
||||
}
|
||||
Generated
-14
@@ -2377,20 +2377,6 @@ dependencies = [
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "comet_api_client"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"reqwest 0.12.23",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"test_data",
|
||||
"tokio",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.7.1"
|
||||
|
||||
@@ -10,7 +10,6 @@ members = [
|
||||
"crates/api_clients/artcraft/artcraft_client",
|
||||
"crates/api_clients/artcraft/artcraft_router",
|
||||
"crates/api_clients/cloud_storage",
|
||||
"crates/api_clients/comet_api_client",
|
||||
"crates/api_clients/datadog_client",
|
||||
"crates/api_clients/fal_client",
|
||||
"crates/api_clients/google_drive_downloader",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS folders;
|
||||
@@ -1,56 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE folders (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Effective "primary key" (PUBLIC, used by the API).
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- User-supplied folder name.
|
||||
name VARCHAR(255) NOT NULL,
|
||||
|
||||
-- The user that owns this folder.
|
||||
owner_user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Parent folder for nesting, NULL for top-level folders.
|
||||
maybe_parent_folder_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- The last four media files added to the folder, in order. These are
|
||||
-- denormalized for cheap thumbnail-grid rendering on folder cards
|
||||
-- without a separate fan-out query.
|
||||
maybe_last_media_file_token_1 VARCHAR(32) DEFAULT NULL,
|
||||
maybe_last_media_file_token_2 VARCHAR(32) DEFAULT NULL,
|
||||
maybe_last_media_file_token_3 VARCHAR(32) DEFAULT NULL,
|
||||
maybe_last_media_file_token_4 VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Optional user-chosen cover image (a media file). Overrides the
|
||||
-- auto-grid thumbnail derived from the last-N media files above.
|
||||
maybe_cover_image_custom_media_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Optional user-chosen color tag. Wide enough for "#RRGGBBAA" plus a
|
||||
-- short named-color fallback.
|
||||
maybe_color_code VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- Whether the user has starred / favorited this folder.
|
||||
has_star BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- ========== TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- Soft-delete timestamp. NULL means the folder is live.
|
||||
maybe_deleted_at TIMESTAMP NULL,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (token),
|
||||
KEY fk_owner_user_token (owner_user_token),
|
||||
KEY fk_maybe_parent_folder_token (maybe_parent_folder_token),
|
||||
KEY index_folders_created_at (created_at),
|
||||
KEY index_folders_updated_at (updated_at),
|
||||
KEY index_maybe_deleted_at (maybe_deleted_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -1,5 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS folder_media_files;
|
||||
@@ -1,27 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE folder_media_files (
|
||||
-- Auto-increment for posterity / row-creation ordering. Not the primary
|
||||
-- key — the composite (folder_token, media_file_token) is. AUTO_INCREMENT
|
||||
-- requires its own key, hence the UNIQUE KEY below.
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- The folder this membership belongs to.
|
||||
folder_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The media file in the folder.
|
||||
media_file_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- When the media file was added to the folder. Rows are hard-deleted
|
||||
-- when a media file is removed, so there is no `maybe_deleted_at`.
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (folder_token, media_file_token),
|
||||
UNIQUE KEY (id),
|
||||
KEY index_folder_token (folder_token),
|
||||
KEY index_media_file_token (media_file_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE generic_inference_jobs
|
||||
DROP COLUMN platform_type;
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE generic_inference_jobs
|
||||
ADD COLUMN platform_type VARCHAR(16) DEFAULT NULL;
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files
|
||||
DROP COLUMN platform_type;
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files
|
||||
ADD COLUMN platform_type VARCHAR(16) DEFAULT NULL;
|
||||
@@ -1,32 +0,0 @@
|
||||
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
|
||||
-- with comments attached to the fields for centralized documentation.
|
||||
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- N:M mapping between `folders` and `media_files`. Rows are hard-deleted
|
||||
-- when a media file is removed from a folder — no soft-delete column.
|
||||
CREATE TABLE folder_media_files (
|
||||
-- Auto-increment for posterity / row-creation ordering. Not the primary
|
||||
-- key — the composite (folder_token, media_file_token) is. AUTO_INCREMENT
|
||||
-- requires its own key, hence the UNIQUE KEY below.
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- The folder this membership belongs to.
|
||||
folder_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The media file in the folder.
|
||||
media_file_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- When the media file was added to the folder. Rows are hard-deleted
|
||||
-- when a media file is removed, so there is no `maybe_deleted_at`.
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (folder_token, media_file_token),
|
||||
UNIQUE KEY (id),
|
||||
KEY index_folder_token (folder_token),
|
||||
KEY index_media_file_token (media_file_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -1,59 +0,0 @@
|
||||
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
|
||||
-- with comments attached to the fields for centralized documentation.
|
||||
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE folders (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Effective "primary key" (PUBLIC, used by the API).
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- User-supplied folder name.
|
||||
name VARCHAR(255) NOT NULL,
|
||||
|
||||
-- The user that owns this folder.
|
||||
owner_user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Parent folder for nesting, NULL for top-level folders.
|
||||
maybe_parent_folder_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- The last four media files added to the folder, in order. These are
|
||||
-- denormalized for cheap thumbnail-grid rendering on folder cards
|
||||
-- without a separate fan-out query.
|
||||
maybe_last_media_file_token_1 VARCHAR(32) DEFAULT NULL,
|
||||
maybe_last_media_file_token_2 VARCHAR(32) DEFAULT NULL,
|
||||
maybe_last_media_file_token_3 VARCHAR(32) DEFAULT NULL,
|
||||
maybe_last_media_file_token_4 VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Optional user-chosen cover image (a media file). Overrides the
|
||||
-- auto-grid thumbnail derived from the last-N media files above.
|
||||
maybe_cover_image_custom_media_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Optional user-chosen color tag. Wide enough for "#RRGGBBAA" plus a
|
||||
-- short named-color fallback.
|
||||
maybe_color_code VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- Whether the user has starred / favorited this folder.
|
||||
has_star BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- ========== TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- Soft-delete timestamp. NULL means the folder is live.
|
||||
maybe_deleted_at TIMESTAMP NULL,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (token),
|
||||
KEY fk_owner_user_token (owner_user_token),
|
||||
KEY fk_maybe_parent_folder_token (maybe_parent_folder_token),
|
||||
KEY index_folders_created_at (created_at),
|
||||
KEY index_folders_updated_at (updated_at),
|
||||
KEY index_maybe_deleted_at (maybe_deleted_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -208,11 +208,6 @@ CREATE TABLE generic_inference_jobs (
|
||||
-- Multiple debug_logs rows can share the same event token.
|
||||
maybe_debug_log_event_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- The platform the enqueuing request came from ('web', 'desktop_app',
|
||||
-- 'curl', 'python_requests', 'postman'),
|
||||
-- inferred from the request's User-Agent header.
|
||||
platform_type VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- ========== JOB SYSTEM DETAILS ==========
|
||||
|
||||
-- Jobs begin as "pending", then transition to other states.
|
||||
|
||||
@@ -65,12 +65,6 @@ CREATE TABLE media_files (
|
||||
-- What service provided the generation (if any).
|
||||
maybe_generation_provider VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- The platform the creating request came from ('web', 'desktop_app',
|
||||
-- 'curl', 'python_requests', 'postman'),
|
||||
-- inferred from the request's User-Agent header. Copied from the originating
|
||||
-- generic_inference_jobs row for inference results.
|
||||
platform_type VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- For inference that can be tied back to a model, the type of model.
|
||||
-- There are multiple indices on this column!
|
||||
-- DO NOT EXPOSE THIS TO USERS VIA THE API, as we may leak secrets in doing so.
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
// NB: The "folder media file" list-item shape lives in storyteller_web
|
||||
// (see `endpoints/folders/media_files/list_folder_media_files_handler.rs`)
|
||||
// because it embeds `MediaFileCoverImageDetails` and other domain types
|
||||
// that depend on the request's `MediaDomain` + `ServerEnvironment`.
|
||||
|
||||
/// Canonical wire shape for a folder. Used by single-folder GETs, create
|
||||
/// responses, list rows, and subfolder list rows.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct FolderInfo {
|
||||
pub token: FolderToken,
|
||||
pub name: String,
|
||||
pub owner_user_token: UserToken,
|
||||
pub maybe_parent_folder_token: Option<FolderToken>,
|
||||
|
||||
/// The four most-recent media files added to the folder, in slot order
|
||||
/// (the schema stores them as `maybe_last_media_file_token_1..4`).
|
||||
/// Slots whose media file has been deleted are skipped, so the vec
|
||||
/// may contain 0-4 entries.
|
||||
pub last_media_thumbnails: Vec<FolderThumbnail>,
|
||||
|
||||
/// User-chosen cover image. When present, the frontend should use this
|
||||
/// as the folder's primary thumbnail instead of the
|
||||
/// `last_media_thumbnails` grid.
|
||||
pub maybe_custom_cover_thumbnail: Option<FolderThumbnail>,
|
||||
|
||||
pub maybe_color_code: Option<String>,
|
||||
pub has_star: bool,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
/// True when `maybe_parent_folder_token` is set but the referenced
|
||||
/// parent row is missing or soft-deleted.
|
||||
pub is_orphaned: bool,
|
||||
}
|
||||
|
||||
/// A compact thumbnail descriptor for a media file referenced by a
|
||||
/// folder (either one of the `last_media_*` slots or the custom cover).
|
||||
///
|
||||
/// Intentionally narrower than `MediaLinks` / `CoverImageLinks` — folders
|
||||
/// only need the bare minimum to render a thumbnail card. The CDN URL is
|
||||
/// the direct asset link; `maybe_thumbnail_template` carries the resizing
|
||||
/// template URL (with `{{width}}` / `{{height}}` placeholders) when the
|
||||
/// media class supports it.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct FolderThumbnail {
|
||||
pub token: MediaFileToken,
|
||||
pub media_class: MediaFileClass,
|
||||
pub media_type: MediaFileType,
|
||||
|
||||
/// Direct CDN URL to the underlying media asset.
|
||||
pub cdn_url: Url,
|
||||
|
||||
/// Resizing template URL (with `{{width}}` / `{{height}}` placeholders)
|
||||
/// when the media class supports it. `None` for media classes without
|
||||
/// a thumbnail variant. (Video media files instead have video previews,
|
||||
/// which, in turn, have their own thumbnail templates.)
|
||||
pub maybe_thumbnail_template: Option<String>,
|
||||
|
||||
/// Video preview images (still and animated gif) for mp4 video files.
|
||||
/// These are only set for video media files.
|
||||
pub maybe_video_previews: Option<FolderThumbnailVideoPreviews>,
|
||||
}
|
||||
|
||||
/// Video preview images for a video media file referenced by a folder.
|
||||
/// Mirrors the shape of `MediaLinks`' video previews, but is its own type
|
||||
/// so the folder wire shape can evolve independently.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct FolderThumbnailVideoPreviews {
|
||||
/// A static single frame preview image of the video.
|
||||
pub still: Url,
|
||||
|
||||
/// An animated gif preview of the video.
|
||||
pub animated: Url,
|
||||
|
||||
/// A template used to construct the still thumbnail URL.
|
||||
/// Replace the string `{WIDTH}` with the desired width.
|
||||
pub still_thumbnail_template: String,
|
||||
|
||||
/// A template used to construct the animated thumbnail URL.
|
||||
/// Replace the string `{WIDTH}` with the desired width.
|
||||
pub animated_thumbnail_template: String,
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::folders::common::FolderInfo;
|
||||
|
||||
// ── POST /v1/folders/create ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct CreateFolderRequest {
|
||||
pub name: String,
|
||||
pub maybe_parent_folder_token: Option<FolderToken>,
|
||||
pub maybe_color_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct CreateFolderSuccessResponse {
|
||||
pub success: bool,
|
||||
pub folder: FolderInfo,
|
||||
}
|
||||
|
||||
// ── GET /v1/folders/list_all ──
|
||||
|
||||
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||
pub struct ListFoldersQueryParams {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListFoldersSuccessResponse {
|
||||
pub success: bool,
|
||||
pub folders: Vec<FolderInfo>,
|
||||
pub maybe_cursor: Option<String>,
|
||||
}
|
||||
|
||||
// ── GET /v1/folders/folder/{folder_token} ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct FolderPathInfo {
|
||||
pub folder_token: FolderToken,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct GetFolderSuccessResponse {
|
||||
pub success: bool,
|
||||
pub folder: FolderInfo,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/folder/{folder_token}/rename ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct RenameFolderRequest {
|
||||
pub new_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct RenameFolderSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/folder/{folder_token}/star ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SetFolderStarRequest {
|
||||
pub has_star: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SetFolderStarSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/folder/{folder_token}/color_code ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SetFolderColorCodeRequest {
|
||||
/// `None` clears the color tag.
|
||||
pub maybe_color_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SetFolderColorCodeSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/folder/{folder_token}/cover_image ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SetFolderCoverImageRequest {
|
||||
/// `None` clears the cover image. If set, the server resolves it to a
|
||||
/// usable cover-image media file token: image/video media files are
|
||||
/// used directly; other types fall back to the file's own
|
||||
/// `maybe_cover_image_media_file_token` (and fail if neither path
|
||||
/// applies).
|
||||
pub maybe_media_file_token: Option<MediaFileToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SetFolderCoverImageSuccessResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// The media file token that ended up being stored as the cover image
|
||||
/// (may differ from the requested token if the server resolved through
|
||||
/// the file's own cover image). `None` when the cover was cleared.
|
||||
pub maybe_resolved_cover_media_file_token: Option<MediaFileToken>,
|
||||
}
|
||||
|
||||
// ── DELETE /v1/folders/folder/{folder_token} ──
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct DeleteFolderSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
// ── GET /v1/folders/media_files/{folder_token} ──
|
||||
//
|
||||
// NB: `ListFolderMediaFilesSuccessResponse` and `FolderMediaFileListItem`
|
||||
// live in `storyteller_web`'s handler because the wire shape embeds
|
||||
// `MediaFileCoverImageDetails` and `MediaLinks` constructors that depend
|
||||
// on the request's `MediaDomain` + `ServerEnvironment`.
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct FolderMediaFilesPathInfo {
|
||||
pub folder_token: FolderToken,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||
pub struct ListFolderMediaFilesQueryParams {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/media_files/{folder_token}/bulk_add ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct BulkAddFolderMediaFilesRequest {
|
||||
/// Media files to add to the folder. Tokens that don't exist or are
|
||||
/// soft-deleted are silently skipped. Already-added rows are no-ops
|
||||
/// (idempotent via INSERT IGNORE on the composite primary key).
|
||||
pub media_file_tokens: Vec<MediaFileToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct BulkAddFolderMediaFilesSuccessResponse {
|
||||
pub success: bool,
|
||||
/// Tokens that pre-existed (existed + not deleted). Already-added rows
|
||||
/// are included since the operation is idempotent.
|
||||
pub accepted_media_file_tokens: Vec<MediaFileToken>,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/media_files/{folder_token}/bulk_remove ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct BulkRemoveFolderMediaFilesRequest {
|
||||
pub media_file_tokens: Vec<MediaFileToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct BulkRemoveFolderMediaFilesSuccessResponse {
|
||||
pub success: bool,
|
||||
/// How many membership rows were actually hard-deleted.
|
||||
pub removed_count: u64,
|
||||
}
|
||||
|
||||
// ── POST /v1/folders/media_files/{folder_token}/bulk_move ──
|
||||
|
||||
/// Atomically move the listed media files from `source_folder` to the
|
||||
/// URL `folder_token` (destination). Idempotent on both sides: tokens
|
||||
/// that aren't currently in the source are silently skipped, and tokens
|
||||
/// that are already in the destination stay there.
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct BulkMoveFolderMediaFilesRequest {
|
||||
pub source_folder: FolderToken,
|
||||
pub media_file_tokens: Vec<MediaFileToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct BulkMoveFolderMediaFilesSuccessResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// Number of membership rows actually deleted from the source folder.
|
||||
pub removed_from_source_count: u64,
|
||||
|
||||
/// Number of membership rows actually inserted into the destination
|
||||
/// folder. May be lower than the input length because (a) tokens that
|
||||
/// were already present in the destination don't insert, and (b)
|
||||
/// tokens whose media file doesn't exist / is soft-deleted are filtered
|
||||
/// out before the insert.
|
||||
pub added_to_destination_count: u64,
|
||||
|
||||
/// The media file tokens that ended up resident in the destination
|
||||
/// folder after the move — i.e. the subset of the input that actually
|
||||
/// referred to live media files. Includes both newly-inserted rows and
|
||||
/// rows that were already there.
|
||||
pub accepted_media_file_tokens: Vec<MediaFileToken>,
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod common;
|
||||
pub mod folder;
|
||||
pub mod media_files;
|
||||
pub mod subfolder;
|
||||
@@ -1,61 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::folders::common::FolderInfo;
|
||||
|
||||
// ── GET /v1/folders/subfolders/{folder_token} ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SubfolderPathInfo {
|
||||
pub folder_token: FolderToken,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||
pub struct ListSubfoldersQueryParams {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListSubfoldersSuccessResponse {
|
||||
pub success: bool,
|
||||
pub subfolders: Vec<FolderInfo>,
|
||||
pub maybe_cursor: Option<String>,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/subfolders/{folder_token}/bulk_add ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct BulkAddSubfoldersRequest {
|
||||
/// The folders to reparent under the URL `folder_token`. Tokens that
|
||||
/// don't exist, aren't owned by the caller, are soft-deleted, or are
|
||||
/// the parent itself are silently skipped (idempotent).
|
||||
pub subfolder_tokens: Vec<FolderToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct BulkAddSubfoldersSuccessResponse {
|
||||
pub success: bool,
|
||||
/// Tokens that were actually accepted (existed + owned + not deleted +
|
||||
/// not the parent itself). Already-parented tokens are included here
|
||||
/// since the operation is idempotent.
|
||||
pub accepted_subfolder_tokens: Vec<FolderToken>,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/subfolders/{folder_token}/bulk_remove ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct BulkRemoveSubfoldersRequest {
|
||||
/// Folders to unparent. Only rows currently parented to the URL
|
||||
/// `folder_token` are affected; others are silently skipped.
|
||||
pub subfolder_tokens: Vec<FolderToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct BulkRemoveSubfoldersSuccessResponse {
|
||||
pub success: bool,
|
||||
/// How many rows were actually unparented.
|
||||
pub removed_count: u64,
|
||||
}
|
||||
@@ -2,7 +2,6 @@ pub mod analytics;
|
||||
pub mod characters;
|
||||
pub mod common;
|
||||
pub mod credits;
|
||||
pub mod folders;
|
||||
pub mod generate;
|
||||
pub mod jobs;
|
||||
pub mod media_file;
|
||||
|
||||
@@ -2,6 +2,5 @@ pub mod alerts;
|
||||
pub mod jobs;
|
||||
pub mod user;
|
||||
pub mod user_referrals;
|
||||
pub mod user_stripe_data;
|
||||
pub mod wallet_ledger_entries;
|
||||
pub mod wallets;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod moderator_get_user_stripe_customer_ids;
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
use enums::common::payments_namespace::PaymentsNamespace;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::users::UserToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const MODERATOR_GET_USER_STRIPE_CUSTOMER_IDS_PATH: &str =
|
||||
"/v1/moderation/user_stripe_data/{user_token}/customer_ids";
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ModeratorGetUserStripeCustomerIdsPathInfo {
|
||||
pub user_token: UserToken,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModeratorGetUserStripeCustomerIdsResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// All stripe customer ids we have on file for the user, across payments
|
||||
/// namespaces. A user may have zero or several: a customer link and/or
|
||||
/// subscriptions per namespace (eg. artcraft), plus legacy fakeyou
|
||||
/// subscriptions. The same customer id can appear once per source.
|
||||
pub customer_ids: Vec<ModeratorUserStripeCustomerIdEntry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModeratorUserStripeCustomerIdEntry {
|
||||
pub stripe_customer_id: String,
|
||||
pub payments_namespace: PaymentsNamespace,
|
||||
pub source: ModeratorStripeCustomerIdSource,
|
||||
|
||||
/// Link to the customer in the Stripe dashboard for the namespace's account.
|
||||
pub stripe_dashboard_url: String,
|
||||
}
|
||||
|
||||
/// Which table the customer id was found in.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ModeratorStripeCustomerIdSource {
|
||||
/// From the `user_stripe_customer_links` table (1:1 per namespace).
|
||||
CustomerLink,
|
||||
/// From the user's rows in the `user_subscriptions` table.
|
||||
Subscription,
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
[package]
|
||||
name = "comet_api_client"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
name = "comet_api_client"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# External
|
||||
log.workspace = true
|
||||
# `multipart` is enabled on top of the workspace feature set because the
|
||||
# CometAPI video creation endpoint takes `multipart/form-data` (the
|
||||
# `input_reference` image fields are binary parts).
|
||||
reqwest = { workspace = true, features = ["multipart"] }
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
test_data = { path = "../../testing/test_data" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
@@ -1,17 +0,0 @@
|
||||
/// A CometAPI key used for Bearer token authentication.
|
||||
#[derive(Clone)]
|
||||
pub struct CometApiKey(pub String);
|
||||
|
||||
impl CometApiKey {
|
||||
pub fn new(api_key: String) -> Self {
|
||||
Self(api_key.trim().to_string())
|
||||
}
|
||||
|
||||
pub fn from_str(api_key: &str) -> Self {
|
||||
Self(api_key.trim().to_string())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod comet_api_key;
|
||||
@@ -1,157 +0,0 @@
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use crate::error::comet_bad_request_api_error::CometBadRequestApiError;
|
||||
use crate::error::comet_error::CometError;
|
||||
use crate::error::comet_generic_api_error::CometGenericApiError;
|
||||
use crate::error::comet_specific_api_error::CometSpecificApiError;
|
||||
|
||||
/// Marker CometAPI returns from `GET /v1/videos/{id}` for unknown task ids.
|
||||
const TASK_NOT_EXIST_MARKER: &str = "task_not_exist";
|
||||
|
||||
/// Categorize a non-success response from the video creation endpoint
|
||||
/// (`POST /v1/videos`).
|
||||
///
|
||||
/// Per the docs, 400s from this endpoint mean the request itself was invalid
|
||||
/// (missing prompt, out-of-range duration, image input on an unsupported
|
||||
/// model) — the caller's fault, surfaced as [`CometBadRequestApiError`].
|
||||
pub fn categorize_create_video_error(
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
) -> CometError {
|
||||
if status_code == StatusCode::UNAUTHORIZED {
|
||||
return CometSpecificApiError::UnauthorizedInvalidApiKey { raw_body: body }.into();
|
||||
}
|
||||
|
||||
if status_code == StatusCode::BAD_REQUEST {
|
||||
return CometBadRequestApiError::InvalidRequestParameters { raw_body: body }.into();
|
||||
}
|
||||
|
||||
CometGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code,
|
||||
body,
|
||||
}.into()
|
||||
}
|
||||
|
||||
/// Categorize a non-success response from the poll endpoint
|
||||
/// (`GET /v1/videos/{id}`).
|
||||
///
|
||||
/// Unlike the creation endpoint, a 400 here means the task id is unknown
|
||||
/// ("task_not_exist") — a specific, well-known cause rather than a
|
||||
/// user-facing bad request.
|
||||
pub fn categorize_get_video_task_error(
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
task_id: &str,
|
||||
) -> CometError {
|
||||
if status_code == StatusCode::UNAUTHORIZED {
|
||||
return CometSpecificApiError::UnauthorizedInvalidApiKey { raw_body: body }.into();
|
||||
}
|
||||
|
||||
if status_code == StatusCode::BAD_REQUEST && body.contains(TASK_NOT_EXIST_MARKER) {
|
||||
return CometSpecificApiError::TaskNotFound {
|
||||
task_id: task_id.to_string(),
|
||||
raw_body: body,
|
||||
}.into();
|
||||
}
|
||||
|
||||
CometGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code,
|
||||
body,
|
||||
}.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod create_video_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bad_request_is_caller_fault() {
|
||||
let body = r#"{"error":{"message":"seconds must be between 4 and 15","type":"invalid_request_error"}}"#.to_string();
|
||||
|
||||
let error = categorize_create_video_error(StatusCode::BAD_REQUEST, body.clone());
|
||||
|
||||
match error {
|
||||
CometError::ApiBadRequest(CometBadRequestApiError::InvalidRequestParameters { raw_body }) => {
|
||||
assert_eq!(raw_body, body);
|
||||
}
|
||||
other => panic!("Expected InvalidRequestParameters, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unauthorized_is_specific() {
|
||||
let body = r#"{"error":{"message":"invalid token","type":"cometapi_error"}}"#.to_string();
|
||||
|
||||
let error = categorize_create_video_error(StatusCode::UNAUTHORIZED, body);
|
||||
|
||||
match error {
|
||||
CometError::ApiSpecific(CometSpecificApiError::UnauthorizedInvalidApiKey { raw_body }) => {
|
||||
assert!(raw_body.contains("invalid token"));
|
||||
}
|
||||
other => panic!("Expected UnauthorizedInvalidApiKey, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_errors_fall_back_to_uncategorized() {
|
||||
let error = categorize_create_video_error(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"upstream timeout".to_string(),
|
||||
);
|
||||
|
||||
match &error {
|
||||
CometError::ApiGeneric(CometGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code, .. }) => {
|
||||
assert_eq!(*status_code, StatusCode::BAD_GATEWAY);
|
||||
}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
assert!(error.is_having_downtime_issues());
|
||||
}
|
||||
}
|
||||
|
||||
mod get_video_task_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn task_not_exist_is_task_not_found() {
|
||||
let body = r#"{"error":{"message":"task_not_exist","type":"cometapi_error"}}"#.to_string();
|
||||
|
||||
let error = categorize_get_video_task_error(StatusCode::BAD_REQUEST, body, "task_abc123");
|
||||
|
||||
match error {
|
||||
CometError::ApiSpecific(CometSpecificApiError::TaskNotFound { task_id, raw_body }) => {
|
||||
assert_eq!(task_id, "task_abc123");
|
||||
assert!(raw_body.contains("task_not_exist"));
|
||||
}
|
||||
other => panic!("Expected TaskNotFound, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_bad_requests_fall_back_to_uncategorized() {
|
||||
let body = r#"{"error":{"message":"something else"}}"#.to_string();
|
||||
|
||||
let error = categorize_get_video_task_error(StatusCode::BAD_REQUEST, body, "task_abc123");
|
||||
|
||||
match error {
|
||||
CometError::ApiGeneric(CometGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }) => {}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unauthorized_is_specific() {
|
||||
let body = r#"{"error":{"message":"invalid token"}}"#.to_string();
|
||||
|
||||
let error = categorize_get_video_task_error(StatusCode::UNAUTHORIZED, body, "task_abc123");
|
||||
|
||||
match error {
|
||||
CometError::ApiSpecific(CometSpecificApiError::UnauthorizedInvalidApiKey { .. }) => {}
|
||||
other => panic!("Expected UnauthorizedInvalidApiKey, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// These represent known "400s" that are the fault of the client sending a
|
||||
/// bad request. They should surface to the end user as 400s.
|
||||
#[derive(Debug)]
|
||||
pub enum CometBadRequestApiError {
|
||||
/// The video creation request was rejected for invalid parameters (eg.
|
||||
/// missing prompt, out-of-range duration, an `input_reference` image on a
|
||||
/// model that doesn't support image input).
|
||||
InvalidRequestParameters { raw_body: String },
|
||||
}
|
||||
|
||||
impl Error for CometBadRequestApiError {}
|
||||
|
||||
impl Display for CometBadRequestApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidRequestParameters { raw_body } => write!(f, "Invalid request parameters: {}", raw_body),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CometClientError {
|
||||
/// An error was encountered building the reqwest client or the request.
|
||||
ReqwestClientError(reqwest::Error),
|
||||
|
||||
/// A request field couldn't be validated or converted to the value
|
||||
/// expected on the wire. Carries the field name and the raw input the
|
||||
/// caller supplied.
|
||||
InvalidRequestField { field: &'static str, raw_value: String, reason: String },
|
||||
}
|
||||
|
||||
impl Error for CometClientError {}
|
||||
|
||||
impl Display for CometClientError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ReqwestClientError(err) => write!(f, "Reqwest client error: {}", err),
|
||||
Self::InvalidRequestField { field, raw_value, reason } => {
|
||||
write!(f, "Invalid value for request field `{}`: {:?} ({})", field, raw_value, reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use crate::error::comet_bad_request_api_error::CometBadRequestApiError;
|
||||
use crate::error::comet_client_error::CometClientError;
|
||||
use crate::error::comet_generic_api_error::CometGenericApiError;
|
||||
use crate::error::comet_specific_api_error::CometSpecificApiError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CometError {
|
||||
/// An error that happened client-side (before or while making the request).
|
||||
Client(CometClientError),
|
||||
|
||||
/// Known "400s" that are the fault of the client sending a bad request.
|
||||
/// These should surface to the end user as 400s.
|
||||
ApiBadRequest(CometBadRequestApiError),
|
||||
|
||||
/// Server-side errors with specific, well-known causes.
|
||||
ApiSpecific(CometSpecificApiError),
|
||||
|
||||
/// Server-side errors with unknown / grab-bag causes.
|
||||
ApiGeneric(CometGenericApiError),
|
||||
}
|
||||
|
||||
impl CometError {
|
||||
pub fn is_having_downtime_issues(&self) -> bool {
|
||||
match self {
|
||||
Self::ApiGeneric(CometGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body: _ }) => {
|
||||
matches!(status_code.as_u16(), 502 | 503 | 504 | 524)
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for CometError {}
|
||||
|
||||
impl Display for CometError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Client(e) => write!(f, "CometClientError: {:?}", e),
|
||||
Self::ApiBadRequest(e) => write!(f, "CometBadRequestApiError: {:?}", e),
|
||||
Self::ApiSpecific(e) => write!(f, "CometSpecificApiError: {:?}", e),
|
||||
Self::ApiGeneric(e) => write!(f, "CometGenericApiError: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CometClientError> for CometError {
|
||||
fn from(error: CometClientError) -> Self {
|
||||
Self::Client(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CometBadRequestApiError> for CometError {
|
||||
fn from(error: CometBadRequestApiError) -> Self {
|
||||
Self::ApiBadRequest(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CometSpecificApiError> for CometError {
|
||||
fn from(error: CometSpecificApiError) -> Self {
|
||||
Self::ApiSpecific(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CometGenericApiError> for CometError {
|
||||
fn from(error: CometGenericApiError) -> Self {
|
||||
Self::ApiGeneric(error)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use reqwest::StatusCode;
|
||||
|
||||
/// Server-side errors with unknown / grab-bag causes.
|
||||
#[derive(Debug)]
|
||||
pub enum CometGenericApiError {
|
||||
/// serde_json::Error, likely from a JSON deserialization schema mismatch.
|
||||
/// Includes the original body.
|
||||
SerdeResponseParseErrorWithBody(serde_json::Error, String),
|
||||
|
||||
/// An uncategorized bad HTTP response with status code and body.
|
||||
UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
},
|
||||
|
||||
/// An uncaught error from the HTTP client (network failures, timeouts, etc.)
|
||||
ReqwestError(reqwest::Error),
|
||||
}
|
||||
|
||||
impl Error for CometGenericApiError {}
|
||||
|
||||
impl Display for CometGenericApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body),
|
||||
Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body),
|
||||
Self::ReqwestError(err) => write!(f, "Reqwest error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// Server-side errors with specific, well-known causes.
|
||||
#[derive(Debug)]
|
||||
pub enum CometSpecificApiError {
|
||||
/// 401: the API key is missing or invalid.
|
||||
UnauthorizedInvalidApiKey { raw_body: String },
|
||||
|
||||
/// The poll endpoint responded "task_not_exist" for the given task id.
|
||||
TaskNotFound { task_id: String, raw_body: String },
|
||||
}
|
||||
|
||||
impl Error for CometSpecificApiError {}
|
||||
|
||||
impl Display for CometSpecificApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::UnauthorizedInvalidApiKey { raw_body } => write!(f, "Unauthorized: invalid or missing API key: {}", raw_body),
|
||||
Self::TaskNotFound { task_id, raw_body } => write!(f, "Task not found: {} ({})", task_id, raw_body),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
pub mod categorize_comet_error;
|
||||
pub mod comet_bad_request_api_error;
|
||||
pub mod comet_client_error;
|
||||
pub mod comet_error;
|
||||
pub mod comet_generic_api_error;
|
||||
pub mod comet_specific_api_error;
|
||||
@@ -1 +0,0 @@
|
||||
pub mod video;
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
use crate::creds::comet_api_key::CometApiKey;
|
||||
use crate::error::comet_client_error::CometClientError;
|
||||
use crate::error::comet_error::CometError;
|
||||
use crate::requests::create_video::create_video::{
|
||||
create_video, CometVideoModelRaw, CometVideoSize, CreateVideoArgs, CreateVideoRequest,
|
||||
};
|
||||
use crate::requests::video_task_status::CometVideoTaskStatus;
|
||||
|
||||
/// Seedance 1.0 Pro supports 2-10 second durations.
|
||||
const MIN_DURATION_SECONDS: u8 = 2;
|
||||
const MAX_DURATION_SECONDS: u8 = 10;
|
||||
|
||||
// ── Args ──
|
||||
|
||||
pub struct GenerateDoubaoSeedance1p0ProArgs<'a> {
|
||||
pub request: GenerateDoubaoSeedance1p0ProRequest,
|
||||
pub api_key: &'a CometApiKey,
|
||||
}
|
||||
|
||||
// ── Request ──
|
||||
|
||||
/// NB: Seedance 1.0 Pro is text-only — `input_reference` images are not
|
||||
/// supported, so this request deliberately has no image field.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateDoubaoSeedance1p0ProRequest {
|
||||
pub prompt: String,
|
||||
|
||||
/// 2-10 seconds. `None` uses the API default (5).
|
||||
pub duration_seconds: Option<u8>,
|
||||
|
||||
pub size: Option<DoubaoSeedance1p0ProSize>,
|
||||
}
|
||||
|
||||
// ── Enums ──
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DoubaoSeedance1p0ProSize {
|
||||
Landscape16x9,
|
||||
Standard4x3,
|
||||
Square1x1,
|
||||
Portrait3x4,
|
||||
Portrait9x16,
|
||||
UltraWide21x9,
|
||||
Exact { width: u32, height: u32 },
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
pub struct GenerateDoubaoSeedance1p0ProResponse {
|
||||
/// Poll `GET /v1/videos/{task_id}` until terminal.
|
||||
pub task_id: String,
|
||||
pub status: CometVideoTaskStatus,
|
||||
}
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
pub async fn generate_doubao_seedance_1p0_pro(
|
||||
args: GenerateDoubaoSeedance1p0ProArgs<'_>,
|
||||
) -> Result<GenerateDoubaoSeedance1p0ProResponse, CometError> {
|
||||
let raw_request = args.request.to_create_video_request()?;
|
||||
|
||||
let result = create_video(CreateVideoArgs {
|
||||
api_key: args.api_key,
|
||||
request: raw_request,
|
||||
}).await?;
|
||||
|
||||
Ok(GenerateDoubaoSeedance1p0ProResponse {
|
||||
task_id: result.task_id,
|
||||
status: result.status,
|
||||
})
|
||||
}
|
||||
|
||||
impl GenerateDoubaoSeedance1p0ProRequest {
|
||||
/// Validate and lower to the generic wire request.
|
||||
pub fn to_create_video_request(&self) -> Result<CreateVideoRequest, CometClientError> {
|
||||
if let Some(seconds) = self.duration_seconds {
|
||||
if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) {
|
||||
return Err(CometClientError::InvalidRequestField {
|
||||
field: "duration_seconds",
|
||||
raw_value: seconds.to_string(),
|
||||
reason: format!("Seedance 1.0 Pro supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CreateVideoRequest {
|
||||
model: CometVideoModelRaw::DoubaoSeedance1p0Pro,
|
||||
prompt: self.prompt.clone(),
|
||||
maybe_seconds: self.duration_seconds,
|
||||
maybe_size: self.size.map(map_size),
|
||||
input_reference_images: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn map_size(size: DoubaoSeedance1p0ProSize) -> CometVideoSize {
|
||||
match size {
|
||||
DoubaoSeedance1p0ProSize::Landscape16x9 => CometVideoSize::Landscape16x9,
|
||||
DoubaoSeedance1p0ProSize::Standard4x3 => CometVideoSize::Standard4x3,
|
||||
DoubaoSeedance1p0ProSize::Square1x1 => CometVideoSize::Square,
|
||||
DoubaoSeedance1p0ProSize::Portrait3x4 => CometVideoSize::Portrait3x4,
|
||||
DoubaoSeedance1p0ProSize::Portrait9x16 => CometVideoSize::Portrait9x16,
|
||||
DoubaoSeedance1p0ProSize::UltraWide21x9 => CometVideoSize::UltraWide21x9,
|
||||
DoubaoSeedance1p0ProSize::Exact { width, height } => CometVideoSize::Exact { width, height },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn maps_to_wire_request() {
|
||||
let request = GenerateDoubaoSeedance1p0ProRequest {
|
||||
prompt: "time lapse of a city at dusk".to_string(),
|
||||
duration_seconds: Some(2),
|
||||
size: Some(DoubaoSeedance1p0ProSize::Square1x1),
|
||||
};
|
||||
|
||||
let raw = request.to_create_video_request().expect("should validate");
|
||||
assert_eq!(raw.text_form_fields(), vec![
|
||||
("model", "doubao-seedance-1-0-pro".to_string()),
|
||||
("prompt", "time lapse of a city at dusk".to_string()),
|
||||
("seconds", "2".to_string()),
|
||||
("size", "1:1".to_string()),
|
||||
]);
|
||||
assert!(raw.input_reference_images.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_bounds() {
|
||||
assert!(request_with_seconds(Some(1)).to_create_video_request().is_err());
|
||||
assert!(request_with_seconds(Some(2)).to_create_video_request().is_ok());
|
||||
assert!(request_with_seconds(Some(10)).to_create_video_request().is_ok());
|
||||
assert!(request_with_seconds(Some(11)).to_create_video_request().is_err());
|
||||
assert!(request_with_seconds(None).to_create_video_request().is_ok());
|
||||
}
|
||||
|
||||
fn request_with_seconds(duration_seconds: Option<u8>) -> GenerateDoubaoSeedance1p0ProRequest {
|
||||
GenerateDoubaoSeedance1p0ProRequest {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
use crate::creds::comet_api_key::CometApiKey;
|
||||
use crate::error::comet_client_error::CometClientError;
|
||||
use crate::error::comet_error::CometError;
|
||||
use crate::requests::create_video::create_video::{
|
||||
create_video, CometVideoModelRaw, CometVideoSize, CreateVideoArgs, CreateVideoRequest,
|
||||
};
|
||||
use crate::requests::video_task_status::CometVideoTaskStatus;
|
||||
|
||||
/// Seedance 1.5 Pro supports 4-12 second durations.
|
||||
const MIN_DURATION_SECONDS: u8 = 4;
|
||||
const MAX_DURATION_SECONDS: u8 = 12;
|
||||
|
||||
// ── Args ──
|
||||
|
||||
pub struct GenerateDoubaoSeedance1p5ProArgs<'a> {
|
||||
pub request: GenerateDoubaoSeedance1p5ProRequest,
|
||||
pub api_key: &'a CometApiKey,
|
||||
}
|
||||
|
||||
// ── Request ──
|
||||
|
||||
/// NB: Seedance 1.5 Pro is text-only — `input_reference` images are not
|
||||
/// supported, so this request deliberately has no image field.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateDoubaoSeedance1p5ProRequest {
|
||||
pub prompt: String,
|
||||
|
||||
/// 4-12 seconds. `None` uses the API default (5).
|
||||
pub duration_seconds: Option<u8>,
|
||||
|
||||
pub size: Option<DoubaoSeedance1p5ProSize>,
|
||||
}
|
||||
|
||||
// ── Enums ──
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DoubaoSeedance1p5ProSize {
|
||||
Landscape16x9,
|
||||
Standard4x3,
|
||||
Square1x1,
|
||||
Portrait3x4,
|
||||
Portrait9x16,
|
||||
UltraWide21x9,
|
||||
Exact { width: u32, height: u32 },
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
pub struct GenerateDoubaoSeedance1p5ProResponse {
|
||||
/// Poll `GET /v1/videos/{task_id}` until terminal.
|
||||
pub task_id: String,
|
||||
pub status: CometVideoTaskStatus,
|
||||
}
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
pub async fn generate_doubao_seedance_1p5_pro(
|
||||
args: GenerateDoubaoSeedance1p5ProArgs<'_>,
|
||||
) -> Result<GenerateDoubaoSeedance1p5ProResponse, CometError> {
|
||||
let raw_request = args.request.to_create_video_request()?;
|
||||
|
||||
let result = create_video(CreateVideoArgs {
|
||||
api_key: args.api_key,
|
||||
request: raw_request,
|
||||
}).await?;
|
||||
|
||||
Ok(GenerateDoubaoSeedance1p5ProResponse {
|
||||
task_id: result.task_id,
|
||||
status: result.status,
|
||||
})
|
||||
}
|
||||
|
||||
impl GenerateDoubaoSeedance1p5ProRequest {
|
||||
/// Validate and lower to the generic wire request.
|
||||
pub fn to_create_video_request(&self) -> Result<CreateVideoRequest, CometClientError> {
|
||||
if let Some(seconds) = self.duration_seconds {
|
||||
if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) {
|
||||
return Err(CometClientError::InvalidRequestField {
|
||||
field: "duration_seconds",
|
||||
raw_value: seconds.to_string(),
|
||||
reason: format!("Seedance 1.5 Pro supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CreateVideoRequest {
|
||||
model: CometVideoModelRaw::DoubaoSeedance1p5Pro,
|
||||
prompt: self.prompt.clone(),
|
||||
maybe_seconds: self.duration_seconds,
|
||||
maybe_size: self.size.map(map_size),
|
||||
input_reference_images: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn map_size(size: DoubaoSeedance1p5ProSize) -> CometVideoSize {
|
||||
match size {
|
||||
DoubaoSeedance1p5ProSize::Landscape16x9 => CometVideoSize::Landscape16x9,
|
||||
DoubaoSeedance1p5ProSize::Standard4x3 => CometVideoSize::Standard4x3,
|
||||
DoubaoSeedance1p5ProSize::Square1x1 => CometVideoSize::Square,
|
||||
DoubaoSeedance1p5ProSize::Portrait3x4 => CometVideoSize::Portrait3x4,
|
||||
DoubaoSeedance1p5ProSize::Portrait9x16 => CometVideoSize::Portrait9x16,
|
||||
DoubaoSeedance1p5ProSize::UltraWide21x9 => CometVideoSize::UltraWide21x9,
|
||||
DoubaoSeedance1p5ProSize::Exact { width, height } => CometVideoSize::Exact { width, height },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn maps_to_wire_request() {
|
||||
let request = GenerateDoubaoSeedance1p5ProRequest {
|
||||
prompt: "a paper boat in a storm drain".to_string(),
|
||||
duration_seconds: Some(12),
|
||||
size: Some(DoubaoSeedance1p5ProSize::Portrait9x16),
|
||||
};
|
||||
|
||||
let raw = request.to_create_video_request().expect("should validate");
|
||||
assert_eq!(raw.text_form_fields(), vec![
|
||||
("model", "doubao-seedance-1-5-pro".to_string()),
|
||||
("prompt", "a paper boat in a storm drain".to_string()),
|
||||
("seconds", "12".to_string()),
|
||||
("size", "9:16".to_string()),
|
||||
]);
|
||||
assert!(raw.input_reference_images.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_bounds() {
|
||||
assert!(request_with_seconds(Some(3)).to_create_video_request().is_err());
|
||||
assert!(request_with_seconds(Some(4)).to_create_video_request().is_ok());
|
||||
assert!(request_with_seconds(Some(12)).to_create_video_request().is_ok());
|
||||
assert!(request_with_seconds(Some(13)).to_create_video_request().is_err());
|
||||
assert!(request_with_seconds(None).to_create_video_request().is_ok());
|
||||
}
|
||||
|
||||
fn request_with_seconds(duration_seconds: Option<u8>) -> GenerateDoubaoSeedance1p5ProRequest {
|
||||
GenerateDoubaoSeedance1p5ProRequest {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
-471
@@ -1,471 +0,0 @@
|
||||
use crate::creds::comet_api_key::CometApiKey;
|
||||
use crate::error::comet_client_error::CometClientError;
|
||||
use crate::error::comet_error::CometError;
|
||||
use crate::requests::create_video::create_video::{
|
||||
create_video, CometInputReferenceImage, CometVideoModelRaw, CometVideoSize,
|
||||
CreateVideoArgs, CreateVideoRequest,
|
||||
};
|
||||
use crate::requests::video_task_status::CometVideoTaskStatus;
|
||||
|
||||
/// Seedance 2.0 supports 4-15 second durations.
|
||||
const MIN_DURATION_SECONDS: u8 = 4;
|
||||
const MAX_DURATION_SECONDS: u8 = 15;
|
||||
|
||||
/// The API default duration when `seconds` is omitted.
|
||||
const DEFAULT_DURATION_SECONDS: u8 = 5;
|
||||
|
||||
// ── Args ──
|
||||
|
||||
pub struct GenerateDoubaoSeedance2p0Args<'a> {
|
||||
pub request: GenerateDoubaoSeedance2p0Request,
|
||||
pub api_key: &'a CometApiKey,
|
||||
}
|
||||
|
||||
// ── Request ──
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateDoubaoSeedance2p0Request {
|
||||
/// Reference attached images in the prompt as `[Image 1]`, `[Image 2]`, etc.
|
||||
pub prompt: String,
|
||||
|
||||
/// 4-15 seconds. `None` uses the API default (5).
|
||||
pub duration_seconds: Option<u8>,
|
||||
|
||||
/// `None` defaults to 16:9 (when a resolution is set).
|
||||
pub aspect_ratio: Option<DoubaoSeedance2p0AspectRatio>,
|
||||
|
||||
/// `None` defaults to 720p (when an aspect ratio is set).
|
||||
pub resolution: Option<DoubaoSeedance2p0Resolution>,
|
||||
|
||||
/// Reference images (JPEG, PNG, WebP) for image-to-video.
|
||||
pub input_reference_images: Vec<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
// ── Enums ──
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DoubaoSeedance2p0AspectRatio {
|
||||
Landscape16x9,
|
||||
Standard4x3,
|
||||
Square1x1,
|
||||
Portrait3x4,
|
||||
Portrait9x16,
|
||||
UltraWide21x9,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DoubaoSeedance2p0Resolution {
|
||||
FourEightyP,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
// ── Pricing ──
|
||||
//
|
||||
// Seedance 2.0 per-second USD pricing (via CometAPI):
|
||||
//
|
||||
// | Resolution | $/sec |
|
||||
// |------------|--------|
|
||||
// | 480p | 0.063 |
|
||||
// | 720p | 0.1368 |
|
||||
// | 1080p | 0.3366 |
|
||||
//
|
||||
// Total cost = price per second × video duration (seconds).
|
||||
|
||||
impl GenerateDoubaoSeedance2p0Request {
|
||||
/// Estimate the USD cost in cents for this generation request.
|
||||
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
|
||||
let dollars_per_second: f64 = match self.resolution {
|
||||
Some(DoubaoSeedance2p0Resolution::FourEightyP) => 0.063,
|
||||
// Default resolution (None) is 720p.
|
||||
Some(DoubaoSeedance2p0Resolution::SevenTwentyP) | None => 0.1368,
|
||||
Some(DoubaoSeedance2p0Resolution::TenEightyP) => 0.3366,
|
||||
};
|
||||
let seconds = self.duration_seconds.unwrap_or(DEFAULT_DURATION_SECONDS);
|
||||
(f64::from(seconds) * dollars_per_second * 100.0).round() as u64
|
||||
}
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
pub struct GenerateDoubaoSeedance2p0Response {
|
||||
/// Poll `GET /v1/videos/{task_id}` until terminal.
|
||||
pub task_id: String,
|
||||
pub status: CometVideoTaskStatus,
|
||||
}
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
pub async fn generate_doubao_seedance_2p0(
|
||||
args: GenerateDoubaoSeedance2p0Args<'_>,
|
||||
) -> Result<GenerateDoubaoSeedance2p0Response, CometError> {
|
||||
let raw_request = args.request.to_create_video_request()?;
|
||||
|
||||
let result = create_video(CreateVideoArgs {
|
||||
api_key: args.api_key,
|
||||
request: raw_request,
|
||||
}).await?;
|
||||
|
||||
Ok(GenerateDoubaoSeedance2p0Response {
|
||||
task_id: result.task_id,
|
||||
status: result.status,
|
||||
})
|
||||
}
|
||||
|
||||
impl GenerateDoubaoSeedance2p0Request {
|
||||
/// Validate and lower to the generic wire request.
|
||||
pub fn to_create_video_request(&self) -> Result<CreateVideoRequest, CometClientError> {
|
||||
if let Some(seconds) = self.duration_seconds {
|
||||
if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) {
|
||||
return Err(CometClientError::InvalidRequestField {
|
||||
field: "duration_seconds",
|
||||
raw_value: seconds.to_string(),
|
||||
reason: format!("Seedance 2.0 supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CreateVideoRequest {
|
||||
model: CometVideoModelRaw::DoubaoSeedance2p0,
|
||||
prompt: self.prompt.clone(),
|
||||
maybe_seconds: self.duration_seconds,
|
||||
maybe_size: self.resolve_size(),
|
||||
input_reference_images: self.input_reference_images.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The wire `size` is a single exact-dimensions field determined by the
|
||||
/// (aspect ratio, resolution) pair. Dimensions mirror CometAPI's own
|
||||
/// playground options for `doubao-seedance-2-0`. When neither knob is
|
||||
/// set, `size` is omitted and the API picks its default.
|
||||
fn resolve_size(&self) -> Option<CometVideoSize> {
|
||||
if self.aspect_ratio.is_none() && self.resolution.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let aspect_ratio = self.aspect_ratio.unwrap_or(DoubaoSeedance2p0AspectRatio::Landscape16x9);
|
||||
let resolution = self.resolution.unwrap_or(DoubaoSeedance2p0Resolution::SevenTwentyP);
|
||||
|
||||
use DoubaoSeedance2p0AspectRatio as Ratio;
|
||||
use DoubaoSeedance2p0Resolution as Res;
|
||||
|
||||
let (width, height) = match (resolution, aspect_ratio) {
|
||||
(Res::FourEightyP, Ratio::Landscape16x9) => (864, 496),
|
||||
(Res::FourEightyP, Ratio::Standard4x3) => (752, 560),
|
||||
(Res::FourEightyP, Ratio::Square1x1) => (640, 640),
|
||||
(Res::FourEightyP, Ratio::Portrait3x4) => (560, 752),
|
||||
(Res::FourEightyP, Ratio::Portrait9x16) => (496, 864),
|
||||
(Res::FourEightyP, Ratio::UltraWide21x9) => (992, 432),
|
||||
|
||||
(Res::SevenTwentyP, Ratio::Landscape16x9) => (1280, 720),
|
||||
(Res::SevenTwentyP, Ratio::Standard4x3) => (1112, 834),
|
||||
(Res::SevenTwentyP, Ratio::Square1x1) => (960, 960),
|
||||
(Res::SevenTwentyP, Ratio::Portrait3x4) => (834, 1112),
|
||||
(Res::SevenTwentyP, Ratio::Portrait9x16) => (720, 1280),
|
||||
(Res::SevenTwentyP, Ratio::UltraWide21x9) => (1470, 630),
|
||||
|
||||
(Res::TenEightyP, Ratio::Landscape16x9) => (1920, 1080),
|
||||
(Res::TenEightyP, Ratio::Standard4x3) => (1664, 1248),
|
||||
(Res::TenEightyP, Ratio::Square1x1) => (1440, 1440),
|
||||
(Res::TenEightyP, Ratio::Portrait3x4) => (1248, 1664),
|
||||
(Res::TenEightyP, Ratio::Portrait9x16) => (1080, 1920),
|
||||
(Res::TenEightyP, Ratio::UltraWide21x9) => (2206, 946),
|
||||
};
|
||||
|
||||
Some(CometVideoSize::Exact { width, height })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use DoubaoSeedance2p0AspectRatio as Ratio;
|
||||
use DoubaoSeedance2p0Resolution as Res;
|
||||
|
||||
#[test]
|
||||
fn maps_to_wire_request() {
|
||||
let request = GenerateDoubaoSeedance2p0Request {
|
||||
prompt: "animate [Image 1]".to_string(),
|
||||
duration_seconds: Some(8),
|
||||
aspect_ratio: Some(Ratio::UltraWide21x9),
|
||||
resolution: Some(Res::TenEightyP),
|
||||
input_reference_images: vec![],
|
||||
};
|
||||
|
||||
let raw = request.to_create_video_request().expect("should validate");
|
||||
assert_eq!(raw.text_form_fields(), vec![
|
||||
("model", "doubao-seedance-2-0".to_string()),
|
||||
("prompt", "animate [Image 1]".to_string()),
|
||||
("seconds", "8".to_string()),
|
||||
("size", "2206x946".to_string()),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_matrix_matches_cometapi_playground() {
|
||||
// (resolution, ratio) -> exact size, as offered by CometAPI's own
|
||||
// playground dropdown for doubao-seedance-2-0.
|
||||
let cases = [
|
||||
(Res::FourEightyP, Ratio::Landscape16x9, "864x496"),
|
||||
(Res::FourEightyP, Ratio::Standard4x3, "752x560"),
|
||||
(Res::FourEightyP, Ratio::Square1x1, "640x640"),
|
||||
(Res::FourEightyP, Ratio::Portrait3x4, "560x752"),
|
||||
(Res::FourEightyP, Ratio::Portrait9x16, "496x864"),
|
||||
(Res::FourEightyP, Ratio::UltraWide21x9, "992x432"),
|
||||
(Res::SevenTwentyP, Ratio::Landscape16x9, "1280x720"),
|
||||
(Res::SevenTwentyP, Ratio::Standard4x3, "1112x834"),
|
||||
(Res::SevenTwentyP, Ratio::Square1x1, "960x960"),
|
||||
(Res::SevenTwentyP, Ratio::Portrait3x4, "834x1112"),
|
||||
(Res::SevenTwentyP, Ratio::Portrait9x16, "720x1280"),
|
||||
(Res::SevenTwentyP, Ratio::UltraWide21x9, "1470x630"),
|
||||
(Res::TenEightyP, Ratio::Landscape16x9, "1920x1080"),
|
||||
(Res::TenEightyP, Ratio::Standard4x3, "1664x1248"),
|
||||
(Res::TenEightyP, Ratio::Square1x1, "1440x1440"),
|
||||
(Res::TenEightyP, Ratio::Portrait3x4, "1248x1664"),
|
||||
(Res::TenEightyP, Ratio::Portrait9x16, "1080x1920"),
|
||||
(Res::TenEightyP, Ratio::UltraWide21x9, "2206x946"),
|
||||
];
|
||||
|
||||
for (resolution, aspect_ratio, expected) in cases {
|
||||
let request = base_request(Some(aspect_ratio), Some(resolution));
|
||||
let size = request.resolve_size().expect("should resolve");
|
||||
assert_eq!(size.as_api_string(), expected,
|
||||
"({resolution:?}, {aspect_ratio:?}) should map to {expected}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_defaults() {
|
||||
// Neither knob set: omit size, let the API default.
|
||||
assert!(base_request(None, None).resolve_size().is_none());
|
||||
|
||||
// Only ratio set: resolution defaults to 720p.
|
||||
let size = base_request(Some(Ratio::Portrait9x16), None).resolve_size().expect("should resolve");
|
||||
assert_eq!(size.as_api_string(), "720x1280");
|
||||
|
||||
// Only resolution set: ratio defaults to 16:9.
|
||||
let size = base_request(None, Some(Res::TenEightyP)).resolve_size().expect("should resolve");
|
||||
assert_eq!(size.as_api_string(), "1920x1080");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_bounds() {
|
||||
let mut request = base_request(None, None);
|
||||
for (seconds, ok) in [(Some(3), false), (Some(4), true), (Some(15), true), (Some(16), false), (None, true)] {
|
||||
request.duration_seconds = seconds;
|
||||
assert_eq!(request.to_create_video_request().is_ok(), ok, "seconds: {seconds:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_estimates() {
|
||||
// 5 seconds at 480p: 5 × $0.063 = $0.315 -> 32¢
|
||||
let mut request = base_request(None, Some(Res::FourEightyP));
|
||||
request.duration_seconds = Some(5);
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), 32);
|
||||
|
||||
// 10 seconds at 1080p: 10 × $0.3366 = $3.366 -> 337¢
|
||||
request.resolution = Some(Res::TenEightyP);
|
||||
request.duration_seconds = Some(10);
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), 337);
|
||||
|
||||
// Defaults (5 seconds, 720p): 5 × $0.1368 = $0.684 -> 68¢
|
||||
request.resolution = None;
|
||||
request.duration_seconds = None;
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), 68);
|
||||
}
|
||||
|
||||
fn base_request(
|
||||
aspect_ratio: Option<DoubaoSeedance2p0AspectRatio>,
|
||||
resolution: Option<DoubaoSeedance2p0Resolution>,
|
||||
) -> GenerateDoubaoSeedance2p0Request {
|
||||
GenerateDoubaoSeedance2p0Request {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds: None,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
input_reference_images: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ⚠️ EXPENSIVE LIVE TESTS ⚠️
|
||||
//
|
||||
// Each test below enqueues a REAL 10-second Seedance 2.0 generation with
|
||||
// four reference images and polls it to completion. These INCUR REAL
|
||||
// COSTS (per CometAPI's per-second pricing):
|
||||
//
|
||||
// 480p: 10s × $0.063 ≈ $0.63
|
||||
// 720p: 10s × $0.1368 ≈ $1.37
|
||||
// 1080p: 10s × $0.3366 ≈ $3.37
|
||||
//
|
||||
// They also take a while — generations have been observed to run well
|
||||
// past 10 minutes. Run one at a time, manually:
|
||||
//
|
||||
// cargo test -p comet_api_client live_generate_480p -- --ignored --nocapture
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
mod live_tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use test_data::web::image_urls::{
|
||||
ERNEST_GHOST_TREX_IMAGE_URL,
|
||||
FOREST_BACKDROP_IMAGE_URL,
|
||||
GHOST_IMAGE_URL,
|
||||
GRASSY_HILL_TRANSPARENT_IMAGE_URL,
|
||||
JUNO_AT_LAKE_IMAGE_URL,
|
||||
MOUNTAIN_TREE_IMAGE_URL,
|
||||
SUPER_WIDE_FALL_MOUNTAINS_IMAGE_URL,
|
||||
TALL_CORGI_SHIBA_TREASURE_OCEAN_URL,
|
||||
TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
|
||||
TREX_SKELETON_IMAGE_URL,
|
||||
WHITE_HOUSE_SUNSET_IMAGE_URL,
|
||||
WIDE_CORGI_SHIBA_TREASURE_OCEAN_URL,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::requests::get_video_task::get_video_task::{get_video_task, GetVideoTaskArgs};
|
||||
use crate::test_utils::load_api_key;
|
||||
|
||||
/// ⚠️ EXPENSIVE: real 10s 480p generation (~$0.63) + a long poll.
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn live_generate_480p_with_references() {
|
||||
let input_reference_images = download_reference_images(&[
|
||||
ERNEST_GHOST_TREX_IMAGE_URL,
|
||||
GHOST_IMAGE_URL,
|
||||
TREX_SKELETON_IMAGE_URL,
|
||||
FOREST_BACKDROP_IMAGE_URL,
|
||||
]).await;
|
||||
|
||||
run_generation_to_completion(GenerateDoubaoSeedance2p0Request {
|
||||
prompt: "The frightened man from [Image 1] runs through the dark forest \
|
||||
from [Image 4], chased by the glowing ghost from [Image 2]. The t-rex \
|
||||
skeleton from [Image 3] rises out of the leaves behind them. Spooky, \
|
||||
comedic 90s horror movie tone."
|
||||
.to_string(),
|
||||
duration_seconds: Some(10),
|
||||
aspect_ratio: Some(DoubaoSeedance2p0AspectRatio::Landscape16x9),
|
||||
resolution: Some(DoubaoSeedance2p0Resolution::FourEightyP),
|
||||
input_reference_images,
|
||||
}).await;
|
||||
}
|
||||
|
||||
/// ⚠️ EXPENSIVE: real 10s 720p generation (~$1.37) + a long poll.
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn live_generate_720p_with_references() {
|
||||
let input_reference_images = download_reference_images(&[
|
||||
JUNO_AT_LAKE_IMAGE_URL,
|
||||
TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
|
||||
TALL_CORGI_SHIBA_TREASURE_OCEAN_URL,
|
||||
WIDE_CORGI_SHIBA_TREASURE_OCEAN_URL,
|
||||
]).await;
|
||||
|
||||
run_generation_to_completion(GenerateDoubaoSeedance2p0Request {
|
||||
prompt: "The dog from [Image 1] and the bespectacled dog from [Image 2] \
|
||||
join the corgi and shiba from [Image 3] on the beach, digging up the \
|
||||
treasure chest from [Image 4] as waves roll in. Joyful, sunny \
|
||||
adventure-film tone."
|
||||
.to_string(),
|
||||
duration_seconds: Some(10),
|
||||
aspect_ratio: Some(DoubaoSeedance2p0AspectRatio::Landscape16x9),
|
||||
resolution: Some(DoubaoSeedance2p0Resolution::SevenTwentyP),
|
||||
input_reference_images,
|
||||
}).await;
|
||||
}
|
||||
|
||||
/// ⚠️ EXPENSIVE: real 10s 1080p generation (~$3.37) + a long poll.
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn live_generate_1080p_with_references() {
|
||||
let input_reference_images = download_reference_images(&[
|
||||
GRASSY_HILL_TRANSPARENT_IMAGE_URL,
|
||||
MOUNTAIN_TREE_IMAGE_URL,
|
||||
SUPER_WIDE_FALL_MOUNTAINS_IMAGE_URL,
|
||||
WHITE_HOUSE_SUNSET_IMAGE_URL,
|
||||
]).await;
|
||||
|
||||
run_generation_to_completion(GenerateDoubaoSeedance2p0Request {
|
||||
prompt: "A cinematic drone shot glides over the grassy hill from \
|
||||
[Image 1], past the lone tree on the mountainside from [Image 2], \
|
||||
across the sweeping autumn mountain range from [Image 3], and settles \
|
||||
on the white house at sunset from [Image 4]. Golden hour, slow and \
|
||||
steady camera movement."
|
||||
.to_string(),
|
||||
duration_seconds: Some(10),
|
||||
aspect_ratio: Some(DoubaoSeedance2p0AspectRatio::Landscape16x9),
|
||||
resolution: Some(DoubaoSeedance2p0Resolution::TenEightyP),
|
||||
input_reference_images,
|
||||
}).await;
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
async fn download_reference_images(urls: &[&str]) -> Vec<CometInputReferenceImage> {
|
||||
let mut images = Vec::with_capacity(urls.len());
|
||||
for url in urls {
|
||||
images.push(download_reference_image(url).await);
|
||||
}
|
||||
images
|
||||
}
|
||||
|
||||
async fn download_reference_image(url: &str) -> CometInputReferenceImage {
|
||||
let response = reqwest::get(url).await.expect("image download should succeed");
|
||||
assert!(response.status().is_success(), "image download failed: {} ({})", url, response.status());
|
||||
|
||||
let filename = url.rsplit('/').next().expect("url should have a path").to_string();
|
||||
let maybe_content_type = if filename.ends_with(".png") {
|
||||
Some("image/png".to_string())
|
||||
} else {
|
||||
Some("image/jpeg".to_string())
|
||||
};
|
||||
let file_bytes = response.bytes().await.expect("image body should download").to_vec();
|
||||
|
||||
println!("Downloaded reference: {} ({} bytes)", filename, file_bytes.len());
|
||||
|
||||
CometInputReferenceImage {
|
||||
file_bytes,
|
||||
filename,
|
||||
maybe_content_type,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enqueue the generation, poll until terminal, and assert success.
|
||||
async fn run_generation_to_completion(request: GenerateDoubaoSeedance2p0Request) {
|
||||
// NB: Generations have been observed to run well past 10 minutes.
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(15);
|
||||
const MAX_POLLS: u32 = 120; // ~30 minutes
|
||||
|
||||
let api_key = load_api_key();
|
||||
println!("Estimated cost: {}¢", request.estimate_cost_in_usd_cents());
|
||||
|
||||
let created = generate_doubao_seedance_2p0(GenerateDoubaoSeedance2p0Args {
|
||||
request,
|
||||
api_key: &api_key,
|
||||
}).await.expect("generate_doubao_seedance_2p0 should succeed");
|
||||
|
||||
println!("Enqueued task: {} (status: {})", created.task_id, created.status);
|
||||
|
||||
for poll in 1..=MAX_POLLS {
|
||||
tokio::time::sleep(POLL_INTERVAL).await;
|
||||
|
||||
let task = get_video_task(GetVideoTaskArgs {
|
||||
api_key: &api_key,
|
||||
task_id: &created.task_id,
|
||||
}).await.expect("get_video_task should succeed");
|
||||
|
||||
println!("Poll {}: status {} (progress: {:?})", poll, task.status, task.maybe_progress);
|
||||
|
||||
if task.status.is_terminal() {
|
||||
assert!(task.status.is_success(), "task should complete successfully: {:?}", task);
|
||||
let video_url = task.maybe_video_url.expect("completed task should have a video url");
|
||||
println!("Video URL: {}", video_url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Task {} did not reach a terminal state in time", created.task_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
-278
@@ -1,278 +0,0 @@
|
||||
use crate::creds::comet_api_key::CometApiKey;
|
||||
use crate::error::comet_client_error::CometClientError;
|
||||
use crate::error::comet_error::CometError;
|
||||
use crate::requests::create_video::create_video::{
|
||||
create_video, CometInputReferenceImage, CometVideoModelRaw, CometVideoSize,
|
||||
CreateVideoArgs, CreateVideoRequest,
|
||||
};
|
||||
use crate::requests::video_task_status::CometVideoTaskStatus;
|
||||
|
||||
/// Seedance 2.0 Fast supports 4-15 second durations.
|
||||
const MIN_DURATION_SECONDS: u8 = 4;
|
||||
const MAX_DURATION_SECONDS: u8 = 15;
|
||||
|
||||
/// The API default duration when `seconds` is omitted.
|
||||
const DEFAULT_DURATION_SECONDS: u8 = 5;
|
||||
|
||||
// ── Args ──
|
||||
|
||||
pub struct GenerateDoubaoSeedance2p0FastArgs<'a> {
|
||||
pub request: GenerateDoubaoSeedance2p0FastRequest,
|
||||
pub api_key: &'a CometApiKey,
|
||||
}
|
||||
|
||||
// ── Request ──
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateDoubaoSeedance2p0FastRequest {
|
||||
/// Reference attached images in the prompt as `[Image 1]`, `[Image 2]`, etc.
|
||||
pub prompt: String,
|
||||
|
||||
/// 4-15 seconds. `None` uses the API default (5).
|
||||
pub duration_seconds: Option<u8>,
|
||||
|
||||
/// `None` defaults to 16:9 (when a resolution is set).
|
||||
pub aspect_ratio: Option<DoubaoSeedance2p0FastAspectRatio>,
|
||||
|
||||
/// `None` defaults to 720p (when an aspect ratio is set).
|
||||
pub resolution: Option<DoubaoSeedance2p0FastResolution>,
|
||||
|
||||
/// Reference images (JPEG, PNG, WebP) for image-to-video.
|
||||
pub input_reference_images: Vec<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
// ── Enums ──
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DoubaoSeedance2p0FastAspectRatio {
|
||||
Landscape16x9,
|
||||
Standard4x3,
|
||||
Square1x1,
|
||||
Portrait3x4,
|
||||
Portrait9x16,
|
||||
UltraWide21x9,
|
||||
}
|
||||
|
||||
/// NB: 1080p is available for all Seedance models EXCEPT 2.0 Fast, so this
|
||||
/// enum deliberately has no 1080p variant.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DoubaoSeedance2p0FastResolution {
|
||||
FourEightyP,
|
||||
SevenTwentyP,
|
||||
}
|
||||
|
||||
// ── Pricing ──
|
||||
//
|
||||
// Seedance 2.0 Fast per-second USD pricing (via CometAPI):
|
||||
//
|
||||
// | Resolution | $/sec |
|
||||
// |------------|--------|
|
||||
// | 480p | 0.0504 |
|
||||
// | 720p | 0.108 |
|
||||
//
|
||||
// Total cost = price per second × video duration (seconds).
|
||||
|
||||
impl GenerateDoubaoSeedance2p0FastRequest {
|
||||
/// Estimate the USD cost in cents for this generation request.
|
||||
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
|
||||
let dollars_per_second: f64 = match self.resolution {
|
||||
Some(DoubaoSeedance2p0FastResolution::FourEightyP) => 0.0504,
|
||||
// Default resolution (None) is 720p.
|
||||
Some(DoubaoSeedance2p0FastResolution::SevenTwentyP) | None => 0.108,
|
||||
};
|
||||
let seconds = self.duration_seconds.unwrap_or(DEFAULT_DURATION_SECONDS);
|
||||
(f64::from(seconds) * dollars_per_second * 100.0).round() as u64
|
||||
}
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
pub struct GenerateDoubaoSeedance2p0FastResponse {
|
||||
/// Poll `GET /v1/videos/{task_id}` until terminal.
|
||||
pub task_id: String,
|
||||
pub status: CometVideoTaskStatus,
|
||||
}
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
pub async fn generate_doubao_seedance_2p0_fast(
|
||||
args: GenerateDoubaoSeedance2p0FastArgs<'_>,
|
||||
) -> Result<GenerateDoubaoSeedance2p0FastResponse, CometError> {
|
||||
let raw_request = args.request.to_create_video_request()?;
|
||||
|
||||
let result = create_video(CreateVideoArgs {
|
||||
api_key: args.api_key,
|
||||
request: raw_request,
|
||||
}).await?;
|
||||
|
||||
Ok(GenerateDoubaoSeedance2p0FastResponse {
|
||||
task_id: result.task_id,
|
||||
status: result.status,
|
||||
})
|
||||
}
|
||||
|
||||
impl GenerateDoubaoSeedance2p0FastRequest {
|
||||
/// Validate and lower to the generic wire request.
|
||||
pub fn to_create_video_request(&self) -> Result<CreateVideoRequest, CometClientError> {
|
||||
if let Some(seconds) = self.duration_seconds {
|
||||
if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) {
|
||||
return Err(CometClientError::InvalidRequestField {
|
||||
field: "duration_seconds",
|
||||
raw_value: seconds.to_string(),
|
||||
reason: format!("Seedance 2.0 Fast supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CreateVideoRequest {
|
||||
model: CometVideoModelRaw::DoubaoSeedance2p0Fast,
|
||||
prompt: self.prompt.clone(),
|
||||
maybe_seconds: self.duration_seconds,
|
||||
maybe_size: self.resolve_size(),
|
||||
input_reference_images: self.input_reference_images.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The wire `size` is a single exact-dimensions field determined by the
|
||||
/// (aspect ratio, resolution) pair. Dimensions mirror CometAPI's own
|
||||
/// playground options for the Seedance 2.0 family. When neither knob is
|
||||
/// set, `size` is omitted and the API picks its default.
|
||||
fn resolve_size(&self) -> Option<CometVideoSize> {
|
||||
if self.aspect_ratio.is_none() && self.resolution.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let aspect_ratio = self.aspect_ratio.unwrap_or(DoubaoSeedance2p0FastAspectRatio::Landscape16x9);
|
||||
let resolution = self.resolution.unwrap_or(DoubaoSeedance2p0FastResolution::SevenTwentyP);
|
||||
|
||||
use DoubaoSeedance2p0FastAspectRatio as Ratio;
|
||||
use DoubaoSeedance2p0FastResolution as Res;
|
||||
|
||||
let (width, height) = match (resolution, aspect_ratio) {
|
||||
(Res::FourEightyP, Ratio::Landscape16x9) => (864, 496),
|
||||
(Res::FourEightyP, Ratio::Standard4x3) => (752, 560),
|
||||
(Res::FourEightyP, Ratio::Square1x1) => (640, 640),
|
||||
(Res::FourEightyP, Ratio::Portrait3x4) => (560, 752),
|
||||
(Res::FourEightyP, Ratio::Portrait9x16) => (496, 864),
|
||||
(Res::FourEightyP, Ratio::UltraWide21x9) => (992, 432),
|
||||
|
||||
(Res::SevenTwentyP, Ratio::Landscape16x9) => (1280, 720),
|
||||
(Res::SevenTwentyP, Ratio::Standard4x3) => (1112, 834),
|
||||
(Res::SevenTwentyP, Ratio::Square1x1) => (960, 960),
|
||||
(Res::SevenTwentyP, Ratio::Portrait3x4) => (834, 1112),
|
||||
(Res::SevenTwentyP, Ratio::Portrait9x16) => (720, 1280),
|
||||
(Res::SevenTwentyP, Ratio::UltraWide21x9) => (1470, 630),
|
||||
};
|
||||
|
||||
Some(CometVideoSize::Exact { width, height })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use DoubaoSeedance2p0FastAspectRatio as Ratio;
|
||||
use DoubaoSeedance2p0FastResolution as Res;
|
||||
|
||||
#[test]
|
||||
fn maps_to_wire_request() {
|
||||
let request = GenerateDoubaoSeedance2p0FastRequest {
|
||||
prompt: "a corgi surfing".to_string(),
|
||||
duration_seconds: Some(4),
|
||||
aspect_ratio: Some(Ratio::Landscape16x9),
|
||||
resolution: Some(Res::FourEightyP),
|
||||
input_reference_images: vec![],
|
||||
};
|
||||
|
||||
let raw = request.to_create_video_request().expect("should validate");
|
||||
assert_eq!(raw.text_form_fields(), vec![
|
||||
("model", "doubao-seedance-2-0-fast".to_string()),
|
||||
("prompt", "a corgi surfing".to_string()),
|
||||
("seconds", "4".to_string()),
|
||||
("size", "864x496".to_string()),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_matrix_matches_cometapi_playground() {
|
||||
// (resolution, ratio) -> exact size, as offered by CometAPI's own
|
||||
// playground dropdown for the Seedance 2.0 family. No 1080p row: Fast
|
||||
// doesn't support it (enforced by the resolution enum).
|
||||
let cases = [
|
||||
(Res::FourEightyP, Ratio::Landscape16x9, "864x496"),
|
||||
(Res::FourEightyP, Ratio::Standard4x3, "752x560"),
|
||||
(Res::FourEightyP, Ratio::Square1x1, "640x640"),
|
||||
(Res::FourEightyP, Ratio::Portrait3x4, "560x752"),
|
||||
(Res::FourEightyP, Ratio::Portrait9x16, "496x864"),
|
||||
(Res::FourEightyP, Ratio::UltraWide21x9, "992x432"),
|
||||
(Res::SevenTwentyP, Ratio::Landscape16x9, "1280x720"),
|
||||
(Res::SevenTwentyP, Ratio::Standard4x3, "1112x834"),
|
||||
(Res::SevenTwentyP, Ratio::Square1x1, "960x960"),
|
||||
(Res::SevenTwentyP, Ratio::Portrait3x4, "834x1112"),
|
||||
(Res::SevenTwentyP, Ratio::Portrait9x16, "720x1280"),
|
||||
(Res::SevenTwentyP, Ratio::UltraWide21x9, "1470x630"),
|
||||
];
|
||||
|
||||
for (resolution, aspect_ratio, expected) in cases {
|
||||
let request = base_request(Some(aspect_ratio), Some(resolution));
|
||||
let size = request.resolve_size().expect("should resolve");
|
||||
assert_eq!(size.as_api_string(), expected,
|
||||
"({resolution:?}, {aspect_ratio:?}) should map to {expected}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_defaults() {
|
||||
// Neither knob set: omit size, let the API default.
|
||||
assert!(base_request(None, None).resolve_size().is_none());
|
||||
|
||||
// Only ratio set: resolution defaults to 720p.
|
||||
let size = base_request(Some(Ratio::Square1x1), None).resolve_size().expect("should resolve");
|
||||
assert_eq!(size.as_api_string(), "960x960");
|
||||
|
||||
// Only resolution set: ratio defaults to 16:9.
|
||||
let size = base_request(None, Some(Res::FourEightyP)).resolve_size().expect("should resolve");
|
||||
assert_eq!(size.as_api_string(), "864x496");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_bounds() {
|
||||
let mut request = base_request(None, None);
|
||||
for (seconds, ok) in [(Some(3), false), (Some(4), true), (Some(15), true), (Some(16), false), (None, true)] {
|
||||
request.duration_seconds = seconds;
|
||||
assert_eq!(request.to_create_video_request().is_ok(), ok, "seconds: {seconds:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_estimates() {
|
||||
// 5 seconds at 480p: 5 × $0.0504 = $0.252 -> 25¢
|
||||
let mut request = base_request(None, Some(Res::FourEightyP));
|
||||
request.duration_seconds = Some(5);
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), 25);
|
||||
|
||||
// 15 seconds at 720p: 15 × $0.108 = $1.62 -> 162¢
|
||||
request.resolution = Some(Res::SevenTwentyP);
|
||||
request.duration_seconds = Some(15);
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), 162);
|
||||
|
||||
// Defaults (5 seconds, 720p): 5 × $0.108 = $0.54 -> 54¢
|
||||
request.resolution = None;
|
||||
request.duration_seconds = None;
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), 54);
|
||||
}
|
||||
|
||||
fn base_request(
|
||||
aspect_ratio: Option<DoubaoSeedance2p0FastAspectRatio>,
|
||||
resolution: Option<DoubaoSeedance2p0FastResolution>,
|
||||
) -> GenerateDoubaoSeedance2p0FastRequest {
|
||||
GenerateDoubaoSeedance2p0FastRequest {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds: None,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
input_reference_images: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
use crate::creds::comet_api_key::CometApiKey;
|
||||
use crate::error::comet_client_error::CometClientError;
|
||||
use crate::error::comet_error::CometError;
|
||||
use crate::requests::create_video::create_video::{
|
||||
create_video, CometInputReferenceImage, CometVideoModelRaw, CometVideoSize,
|
||||
CreateVideoArgs, CreateVideoRequest,
|
||||
};
|
||||
use crate::requests::video_task_status::CometVideoTaskStatus;
|
||||
|
||||
/// Vidu Q3 supports 1-16 second durations.
|
||||
const MIN_DURATION_SECONDS: u8 = 1;
|
||||
const MAX_DURATION_SECONDS: u8 = 16;
|
||||
|
||||
// ── Args ──
|
||||
|
||||
pub struct GenerateViduQ3Args<'a> {
|
||||
pub request: GenerateViduQ3Request,
|
||||
pub api_key: &'a CometApiKey,
|
||||
}
|
||||
|
||||
// ── Request ──
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateViduQ3Request {
|
||||
pub prompt: String,
|
||||
|
||||
/// 1-16 seconds. `None` uses the API default (5).
|
||||
pub duration_seconds: Option<u8>,
|
||||
|
||||
pub size: Option<ViduQ3Size>,
|
||||
|
||||
/// Reference images for image-to-video / start-end-to-video workflows.
|
||||
pub input_reference_images: Vec<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
// ── Enums ──
|
||||
|
||||
/// Vidu Q3 renders at 540p/720p/1080p (24 FPS). The API's `size` field takes
|
||||
/// an aspect ratio preset or exact dimensions (eg. 960x528).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ViduQ3Size {
|
||||
Landscape16x9,
|
||||
Standard4x3,
|
||||
Square1x1,
|
||||
Portrait3x4,
|
||||
Portrait9x16,
|
||||
Exact { width: u32, height: u32 },
|
||||
}
|
||||
|
||||
// ── Pricing ──
|
||||
//
|
||||
// Vidu Q3 per-second USD pricing (via CometAPI):
|
||||
//
|
||||
// | Resolution | $/sec |
|
||||
// |-------------|---------|
|
||||
// | 360p/540p | 0.056 |
|
||||
// | 720p/1080p | 0.1232 |
|
||||
//
|
||||
// Total cost = price per second × video duration (seconds).
|
||||
// NB: Not modeled as an estimator because the `size` field is an aspect
|
||||
// ratio in the common case, which doesn't determine the billed resolution.
|
||||
|
||||
// ── Response ──
|
||||
|
||||
pub struct GenerateViduQ3Response {
|
||||
/// Poll `GET /v1/videos/{task_id}` until terminal.
|
||||
pub task_id: String,
|
||||
pub status: CometVideoTaskStatus,
|
||||
}
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
pub async fn generate_vidu_q3(
|
||||
args: GenerateViduQ3Args<'_>,
|
||||
) -> Result<GenerateViduQ3Response, CometError> {
|
||||
let raw_request = args.request.to_create_video_request()?;
|
||||
|
||||
let result = create_video(CreateVideoArgs {
|
||||
api_key: args.api_key,
|
||||
request: raw_request,
|
||||
}).await?;
|
||||
|
||||
Ok(GenerateViduQ3Response {
|
||||
task_id: result.task_id,
|
||||
status: result.status,
|
||||
})
|
||||
}
|
||||
|
||||
impl GenerateViduQ3Request {
|
||||
/// Validate and lower to the generic wire request.
|
||||
pub fn to_create_video_request(&self) -> Result<CreateVideoRequest, CometClientError> {
|
||||
if let Some(seconds) = self.duration_seconds {
|
||||
if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) {
|
||||
return Err(CometClientError::InvalidRequestField {
|
||||
field: "duration_seconds",
|
||||
raw_value: seconds.to_string(),
|
||||
reason: format!("Vidu Q3 supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CreateVideoRequest {
|
||||
model: CometVideoModelRaw::ViduQ3,
|
||||
prompt: self.prompt.clone(),
|
||||
maybe_seconds: self.duration_seconds,
|
||||
maybe_size: self.size.map(map_size),
|
||||
input_reference_images: self.input_reference_images.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn map_size(size: ViduQ3Size) -> CometVideoSize {
|
||||
match size {
|
||||
ViduQ3Size::Landscape16x9 => CometVideoSize::Landscape16x9,
|
||||
ViduQ3Size::Standard4x3 => CometVideoSize::Standard4x3,
|
||||
ViduQ3Size::Square1x1 => CometVideoSize::Square,
|
||||
ViduQ3Size::Portrait3x4 => CometVideoSize::Portrait3x4,
|
||||
ViduQ3Size::Portrait9x16 => CometVideoSize::Portrait9x16,
|
||||
ViduQ3Size::Exact { width, height } => CometVideoSize::Exact { width, height },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn maps_to_wire_request() {
|
||||
let request = GenerateViduQ3Request {
|
||||
prompt: "a lighthouse in fog".to_string(),
|
||||
duration_seconds: Some(16),
|
||||
size: Some(ViduQ3Size::Exact { width: 960, height: 528 }),
|
||||
input_reference_images: vec![],
|
||||
};
|
||||
|
||||
let raw = request.to_create_video_request().expect("should validate");
|
||||
assert_eq!(raw.text_form_fields(), vec![
|
||||
("model", "viduq3".to_string()),
|
||||
("prompt", "a lighthouse in fog".to_string()),
|
||||
("seconds", "16".to_string()),
|
||||
("size", "960x528".to_string()),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_bounds() {
|
||||
assert!(request_with_seconds(Some(0)).to_create_video_request().is_err());
|
||||
assert!(request_with_seconds(Some(1)).to_create_video_request().is_ok());
|
||||
assert!(request_with_seconds(Some(16)).to_create_video_request().is_ok());
|
||||
assert!(request_with_seconds(Some(17)).to_create_video_request().is_err());
|
||||
assert!(request_with_seconds(None).to_create_video_request().is_ok());
|
||||
}
|
||||
|
||||
fn request_with_seconds(duration_seconds: Option<u8>) -> GenerateViduQ3Request {
|
||||
GenerateViduQ3Request {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
input_reference_images: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
use crate::creds::comet_api_key::CometApiKey;
|
||||
use crate::error::comet_client_error::CometClientError;
|
||||
use crate::error::comet_error::CometError;
|
||||
use crate::requests::create_video::create_video::{
|
||||
create_video, CometInputReferenceImage, CometVideoModelRaw, CometVideoSize,
|
||||
CreateVideoArgs, CreateVideoRequest,
|
||||
};
|
||||
use crate::requests::video_task_status::CometVideoTaskStatus;
|
||||
|
||||
/// Vidu Q3 Turbo supports 1-16 second durations.
|
||||
const MIN_DURATION_SECONDS: u8 = 1;
|
||||
const MAX_DURATION_SECONDS: u8 = 16;
|
||||
|
||||
// ── Args ──
|
||||
|
||||
pub struct GenerateViduQ3TurboArgs<'a> {
|
||||
pub request: GenerateViduQ3TurboRequest,
|
||||
pub api_key: &'a CometApiKey,
|
||||
}
|
||||
|
||||
// ── Request ──
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateViduQ3TurboRequest {
|
||||
pub prompt: String,
|
||||
|
||||
/// 1-16 seconds. `None` uses the API default (5).
|
||||
pub duration_seconds: Option<u8>,
|
||||
|
||||
pub size: Option<ViduQ3TurboSize>,
|
||||
|
||||
/// Reference images for image-to-video / start-end-to-video workflows.
|
||||
pub input_reference_images: Vec<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
// ── Enums ──
|
||||
|
||||
/// Vidu Q3 Turbo renders at 540p/720p/1080p (24 FPS). The API's `size` field
|
||||
/// takes an aspect ratio preset or exact dimensions (eg. 960x528).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ViduQ3TurboSize {
|
||||
Landscape16x9,
|
||||
Standard4x3,
|
||||
Square1x1,
|
||||
Portrait3x4,
|
||||
Portrait9x16,
|
||||
Exact { width: u32, height: u32 },
|
||||
}
|
||||
|
||||
// ── Pricing ──
|
||||
//
|
||||
// Vidu Q3 Turbo per-second USD pricing (via CometAPI):
|
||||
//
|
||||
// | Resolution | $/sec |
|
||||
// |-------------|---------|
|
||||
// | 360p/540p | 0.028 |
|
||||
// | 720p/1080p | 0.0616 |
|
||||
//
|
||||
// Total cost = price per second × video duration (seconds).
|
||||
// NB: Not modeled as an estimator because the `size` field is an aspect
|
||||
// ratio in the common case, which doesn't determine the billed resolution.
|
||||
|
||||
// ── Response ──
|
||||
|
||||
pub struct GenerateViduQ3TurboResponse {
|
||||
/// Poll `GET /v1/videos/{task_id}` until terminal.
|
||||
pub task_id: String,
|
||||
pub status: CometVideoTaskStatus,
|
||||
}
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
pub async fn generate_vidu_q3_turbo(
|
||||
args: GenerateViduQ3TurboArgs<'_>,
|
||||
) -> Result<GenerateViduQ3TurboResponse, CometError> {
|
||||
let raw_request = args.request.to_create_video_request()?;
|
||||
|
||||
let result = create_video(CreateVideoArgs {
|
||||
api_key: args.api_key,
|
||||
request: raw_request,
|
||||
}).await?;
|
||||
|
||||
Ok(GenerateViduQ3TurboResponse {
|
||||
task_id: result.task_id,
|
||||
status: result.status,
|
||||
})
|
||||
}
|
||||
|
||||
impl GenerateViduQ3TurboRequest {
|
||||
/// Validate and lower to the generic wire request.
|
||||
pub fn to_create_video_request(&self) -> Result<CreateVideoRequest, CometClientError> {
|
||||
if let Some(seconds) = self.duration_seconds {
|
||||
if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) {
|
||||
return Err(CometClientError::InvalidRequestField {
|
||||
field: "duration_seconds",
|
||||
raw_value: seconds.to_string(),
|
||||
reason: format!("Vidu Q3 Turbo supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CreateVideoRequest {
|
||||
model: CometVideoModelRaw::ViduQ3Turbo,
|
||||
prompt: self.prompt.clone(),
|
||||
maybe_seconds: self.duration_seconds,
|
||||
maybe_size: self.size.map(map_size),
|
||||
input_reference_images: self.input_reference_images.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn map_size(size: ViduQ3TurboSize) -> CometVideoSize {
|
||||
match size {
|
||||
ViduQ3TurboSize::Landscape16x9 => CometVideoSize::Landscape16x9,
|
||||
ViduQ3TurboSize::Standard4x3 => CometVideoSize::Standard4x3,
|
||||
ViduQ3TurboSize::Square1x1 => CometVideoSize::Square,
|
||||
ViduQ3TurboSize::Portrait3x4 => CometVideoSize::Portrait3x4,
|
||||
ViduQ3TurboSize::Portrait9x16 => CometVideoSize::Portrait9x16,
|
||||
ViduQ3TurboSize::Exact { width, height } => CometVideoSize::Exact { width, height },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn maps_to_wire_request() {
|
||||
let request = GenerateViduQ3TurboRequest {
|
||||
prompt: "raindrops on a window".to_string(),
|
||||
duration_seconds: Some(1),
|
||||
size: Some(ViduQ3TurboSize::Portrait9x16),
|
||||
input_reference_images: vec![],
|
||||
};
|
||||
|
||||
let raw = request.to_create_video_request().expect("should validate");
|
||||
assert_eq!(raw.text_form_fields(), vec![
|
||||
("model", "viduq3-turbo".to_string()),
|
||||
("prompt", "raindrops on a window".to_string()),
|
||||
("seconds", "1".to_string()),
|
||||
("size", "9:16".to_string()),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_bounds() {
|
||||
assert!(request_with_seconds(Some(0)).to_create_video_request().is_err());
|
||||
assert!(request_with_seconds(Some(1)).to_create_video_request().is_ok());
|
||||
assert!(request_with_seconds(Some(16)).to_create_video_request().is_ok());
|
||||
assert!(request_with_seconds(Some(17)).to_create_video_request().is_err());
|
||||
assert!(request_with_seconds(None).to_create_video_request().is_ok());
|
||||
}
|
||||
|
||||
fn request_with_seconds(duration_seconds: Option<u8>) -> GenerateViduQ3TurboRequest {
|
||||
GenerateViduQ3TurboRequest {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
input_reference_images: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
use crate::creds::comet_api_key::CometApiKey;
|
||||
use crate::error::comet_client_error::CometClientError;
|
||||
use crate::error::comet_error::CometError;
|
||||
use crate::requests::create_video::create_video::{
|
||||
create_video, CometInputReferenceImage, CometVideoModelRaw, CometVideoSize,
|
||||
CreateVideoArgs, CreateVideoRequest,
|
||||
};
|
||||
use crate::requests::video_task_status::CometVideoTaskStatus;
|
||||
|
||||
/// Wan 2.6 supports 2-15 second durations.
|
||||
const MIN_DURATION_SECONDS: u8 = 2;
|
||||
const MAX_DURATION_SECONDS: u8 = 15;
|
||||
|
||||
/// The API default duration when `seconds` is omitted.
|
||||
const DEFAULT_DURATION_SECONDS: u8 = 5;
|
||||
|
||||
/// Wan 2.6 accepts up to 5 references.
|
||||
const MAX_REFERENCE_IMAGES: usize = 5;
|
||||
|
||||
// ── Args ──
|
||||
|
||||
pub struct GenerateWan2p6Args<'a> {
|
||||
pub request: GenerateWan2p6Request,
|
||||
pub api_key: &'a CometApiKey,
|
||||
}
|
||||
|
||||
// ── Request ──
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateWan2p6Request {
|
||||
pub prompt: String,
|
||||
|
||||
/// 2-15 seconds. `None` uses the API default (5).
|
||||
pub duration_seconds: Option<u8>,
|
||||
|
||||
/// `None` uses the API default (720p).
|
||||
pub size: Option<Wan2p6Size>,
|
||||
|
||||
/// Reference images for image-to-video and reference-to-video workflows.
|
||||
/// Wan 2.6 accepts up to 5 references.
|
||||
pub input_reference_images: Vec<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
// ── Enums ──
|
||||
|
||||
/// Wan 2.6 renders at 720p or 1080p.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Wan2p6Size {
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
// ── Pricing ──
|
||||
//
|
||||
// Wan 2.6 per-second USD pricing (via CometAPI):
|
||||
//
|
||||
// | Resolution | $/sec |
|
||||
// |------------|-------|
|
||||
// | 720p | 0.08 |
|
||||
// | 1080p | 0.12 |
|
||||
//
|
||||
// Total cost = price per second × video duration (seconds).
|
||||
|
||||
impl GenerateWan2p6Request {
|
||||
/// Estimate the USD cost in cents for this generation request.
|
||||
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
|
||||
let cents_per_second: u64 = match self.size {
|
||||
Some(Wan2p6Size::TenEightyP) => 12,
|
||||
// Default resolution (None) is 720p.
|
||||
Some(Wan2p6Size::SevenTwentyP) | None => 8,
|
||||
};
|
||||
let seconds = self.duration_seconds.unwrap_or(DEFAULT_DURATION_SECONDS);
|
||||
u64::from(seconds) * cents_per_second
|
||||
}
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
pub struct GenerateWan2p6Response {
|
||||
/// Poll `GET /v1/videos/{task_id}` until terminal.
|
||||
pub task_id: String,
|
||||
pub status: CometVideoTaskStatus,
|
||||
}
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
pub async fn generate_wan_2p6(
|
||||
args: GenerateWan2p6Args<'_>,
|
||||
) -> Result<GenerateWan2p6Response, CometError> {
|
||||
let raw_request = args.request.to_create_video_request()?;
|
||||
|
||||
let result = create_video(CreateVideoArgs {
|
||||
api_key: args.api_key,
|
||||
request: raw_request,
|
||||
}).await?;
|
||||
|
||||
Ok(GenerateWan2p6Response {
|
||||
task_id: result.task_id,
|
||||
status: result.status,
|
||||
})
|
||||
}
|
||||
|
||||
impl GenerateWan2p6Request {
|
||||
/// Validate and lower to the generic wire request.
|
||||
pub fn to_create_video_request(&self) -> Result<CreateVideoRequest, CometClientError> {
|
||||
if let Some(seconds) = self.duration_seconds {
|
||||
if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) {
|
||||
return Err(CometClientError::InvalidRequestField {
|
||||
field: "duration_seconds",
|
||||
raw_value: seconds.to_string(),
|
||||
reason: format!("Wan 2.6 supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self.input_reference_images.len() > MAX_REFERENCE_IMAGES {
|
||||
return Err(CometClientError::InvalidRequestField {
|
||||
field: "input_reference_images",
|
||||
raw_value: self.input_reference_images.len().to_string(),
|
||||
reason: format!("Wan 2.6 accepts at most {MAX_REFERENCE_IMAGES} references"),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(CreateVideoRequest {
|
||||
model: CometVideoModelRaw::Wan2p6,
|
||||
prompt: self.prompt.clone(),
|
||||
maybe_seconds: self.duration_seconds,
|
||||
maybe_size: self.size.map(map_size),
|
||||
input_reference_images: self.input_reference_images.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn map_size(size: Wan2p6Size) -> CometVideoSize {
|
||||
match size {
|
||||
Wan2p6Size::SevenTwentyP => CometVideoSize::Exact { width: 1280, height: 720 },
|
||||
Wan2p6Size::TenEightyP => CometVideoSize::Exact { width: 1920, height: 1080 },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn maps_to_wire_request() {
|
||||
let request = GenerateWan2p6Request {
|
||||
prompt: "a steam train crossing a viaduct".to_string(),
|
||||
duration_seconds: Some(15),
|
||||
size: Some(Wan2p6Size::TenEightyP),
|
||||
input_reference_images: vec![],
|
||||
};
|
||||
|
||||
let raw = request.to_create_video_request().expect("should validate");
|
||||
assert_eq!(raw.text_form_fields(), vec![
|
||||
("model", "wan2.6".to_string()),
|
||||
("prompt", "a steam train crossing a viaduct".to_string()),
|
||||
("seconds", "15".to_string()),
|
||||
("size", "1920x1080".to_string()),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_bounds() {
|
||||
assert!(request_with_seconds(Some(1)).to_create_video_request().is_err());
|
||||
assert!(request_with_seconds(Some(2)).to_create_video_request().is_ok());
|
||||
assert!(request_with_seconds(Some(15)).to_create_video_request().is_ok());
|
||||
assert!(request_with_seconds(Some(16)).to_create_video_request().is_err());
|
||||
assert!(request_with_seconds(None).to_create_video_request().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_many_references_rejected() {
|
||||
let mut request = request_with_seconds(Some(5));
|
||||
request.input_reference_images = (0..6)
|
||||
.map(|i| CometInputReferenceImage {
|
||||
file_bytes: vec![0u8],
|
||||
filename: format!("ref_{i}.png"),
|
||||
maybe_content_type: Some("image/png".to_string()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let error = request.to_create_video_request().expect_err("should reject 6 references");
|
||||
assert!(matches!(error, CometClientError::InvalidRequestField { field: "input_reference_images", .. }));
|
||||
|
||||
request.input_reference_images.truncate(5);
|
||||
assert!(request.to_create_video_request().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_estimates() {
|
||||
// 5 seconds at 720p: 5 × 8¢ = 40¢
|
||||
let mut request = request_with_seconds(Some(5));
|
||||
request.size = Some(Wan2p6Size::SevenTwentyP);
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), 40);
|
||||
|
||||
// 10 seconds at 1080p: 10 × 12¢ = 120¢
|
||||
request.duration_seconds = Some(10);
|
||||
request.size = Some(Wan2p6Size::TenEightyP);
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), 120);
|
||||
|
||||
// Defaults (5 seconds, 720p): 40¢
|
||||
request.duration_seconds = None;
|
||||
request.size = None;
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), 40);
|
||||
}
|
||||
|
||||
fn request_with_seconds(duration_seconds: Option<u8>) -> GenerateWan2p6Request {
|
||||
GenerateWan2p6Request {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
input_reference_images: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
use crate::creds::comet_api_key::CometApiKey;
|
||||
use crate::error::comet_client_error::CometClientError;
|
||||
use crate::error::comet_error::CometError;
|
||||
use crate::requests::create_video::create_video::{
|
||||
create_video, CometInputReferenceImage, CometVideoModelRaw, CometVideoSize,
|
||||
CreateVideoArgs, CreateVideoRequest,
|
||||
};
|
||||
use crate::requests::video_task_status::CometVideoTaskStatus;
|
||||
|
||||
/// Wan 2.7 supports 2-15 second durations.
|
||||
const MIN_DURATION_SECONDS: u8 = 2;
|
||||
const MAX_DURATION_SECONDS: u8 = 15;
|
||||
|
||||
/// The API default duration when `seconds` is omitted.
|
||||
const DEFAULT_DURATION_SECONDS: u8 = 5;
|
||||
|
||||
// ── Args ──
|
||||
|
||||
pub struct GenerateWan2p7Args<'a> {
|
||||
pub request: GenerateWan2p7Request,
|
||||
pub api_key: &'a CometApiKey,
|
||||
}
|
||||
|
||||
// ── Request ──
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateWan2p7Request {
|
||||
pub prompt: String,
|
||||
|
||||
/// 2-15 seconds. `None` uses the API default (5).
|
||||
pub duration_seconds: Option<u8>,
|
||||
|
||||
/// `None` uses the API default (720p).
|
||||
pub size: Option<Wan2p7Size>,
|
||||
|
||||
/// Reference images for image-to-video and first/last-frame workflows.
|
||||
pub input_reference_images: Vec<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
// ── Enums ──
|
||||
|
||||
/// Wan 2.7 renders at 720p or 1080p.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Wan2p7Size {
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
// ── Pricing ──
|
||||
//
|
||||
// Wan 2.7 per-second USD pricing (via CometAPI):
|
||||
//
|
||||
// | Resolution | $/sec |
|
||||
// |------------|-------|
|
||||
// | 720p | 0.08 |
|
||||
// | 1080p | 0.12 |
|
||||
//
|
||||
// Total cost = price per second × video duration (seconds).
|
||||
|
||||
impl GenerateWan2p7Request {
|
||||
/// Estimate the USD cost in cents for this generation request.
|
||||
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
|
||||
let cents_per_second: u64 = match self.size {
|
||||
Some(Wan2p7Size::TenEightyP) => 12,
|
||||
// Default resolution (None) is 720p.
|
||||
Some(Wan2p7Size::SevenTwentyP) | None => 8,
|
||||
};
|
||||
let seconds = self.duration_seconds.unwrap_or(DEFAULT_DURATION_SECONDS);
|
||||
u64::from(seconds) * cents_per_second
|
||||
}
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
pub struct GenerateWan2p7Response {
|
||||
/// Poll `GET /v1/videos/{task_id}` until terminal.
|
||||
pub task_id: String,
|
||||
pub status: CometVideoTaskStatus,
|
||||
}
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
pub async fn generate_wan_2p7(
|
||||
args: GenerateWan2p7Args<'_>,
|
||||
) -> Result<GenerateWan2p7Response, CometError> {
|
||||
let raw_request = args.request.to_create_video_request()?;
|
||||
|
||||
let result = create_video(CreateVideoArgs {
|
||||
api_key: args.api_key,
|
||||
request: raw_request,
|
||||
}).await?;
|
||||
|
||||
Ok(GenerateWan2p7Response {
|
||||
task_id: result.task_id,
|
||||
status: result.status,
|
||||
})
|
||||
}
|
||||
|
||||
impl GenerateWan2p7Request {
|
||||
/// Validate and lower to the generic wire request.
|
||||
pub fn to_create_video_request(&self) -> Result<CreateVideoRequest, CometClientError> {
|
||||
if let Some(seconds) = self.duration_seconds {
|
||||
if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) {
|
||||
return Err(CometClientError::InvalidRequestField {
|
||||
field: "duration_seconds",
|
||||
raw_value: seconds.to_string(),
|
||||
reason: format!("Wan 2.7 supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CreateVideoRequest {
|
||||
model: CometVideoModelRaw::Wan2p7,
|
||||
prompt: self.prompt.clone(),
|
||||
maybe_seconds: self.duration_seconds,
|
||||
maybe_size: self.size.map(map_size),
|
||||
input_reference_images: self.input_reference_images.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn map_size(size: Wan2p7Size) -> CometVideoSize {
|
||||
match size {
|
||||
Wan2p7Size::SevenTwentyP => CometVideoSize::Exact { width: 1280, height: 720 },
|
||||
Wan2p7Size::TenEightyP => CometVideoSize::Exact { width: 1920, height: 1080 },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn maps_to_wire_request() {
|
||||
let request = GenerateWan2p7Request {
|
||||
prompt: "a koi pond at golden hour".to_string(),
|
||||
duration_seconds: Some(5),
|
||||
size: Some(Wan2p7Size::SevenTwentyP),
|
||||
input_reference_images: vec![],
|
||||
};
|
||||
|
||||
let raw = request.to_create_video_request().expect("should validate");
|
||||
assert_eq!(raw.text_form_fields(), vec![
|
||||
("model", "wan2.7".to_string()),
|
||||
("prompt", "a koi pond at golden hour".to_string()),
|
||||
("seconds", "5".to_string()),
|
||||
("size", "1280x720".to_string()),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_bounds() {
|
||||
assert!(request_with_seconds(Some(1)).to_create_video_request().is_err());
|
||||
assert!(request_with_seconds(Some(2)).to_create_video_request().is_ok());
|
||||
assert!(request_with_seconds(Some(15)).to_create_video_request().is_ok());
|
||||
assert!(request_with_seconds(Some(16)).to_create_video_request().is_err());
|
||||
assert!(request_with_seconds(None).to_create_video_request().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_estimates() {
|
||||
// 5 seconds at 720p: 5 × 8¢ = 40¢
|
||||
let mut request = request_with_seconds(Some(5));
|
||||
request.size = Some(Wan2p7Size::SevenTwentyP);
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), 40);
|
||||
|
||||
// 10 seconds at 1080p: 10 × 12¢ = 120¢
|
||||
request.duration_seconds = Some(10);
|
||||
request.size = Some(Wan2p7Size::TenEightyP);
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), 120);
|
||||
|
||||
// Defaults (5 seconds, 720p): 40¢
|
||||
request.duration_seconds = None;
|
||||
request.size = None;
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), 40);
|
||||
}
|
||||
|
||||
fn request_with_seconds(duration_seconds: Option<u8>) -> GenerateWan2p7Request {
|
||||
GenerateWan2p7Request {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
input_reference_images: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
//! Concrete, strongly-typed per-model bindings over the generic
|
||||
//! [`crate::requests::create_video`] call. Each module encodes one model's
|
||||
//! constraints (duration range, size options, image-input support) in the
|
||||
//! type system and validates before hitting the wire.
|
||||
|
||||
pub mod generate_doubao_seedance_1p0_pro;
|
||||
pub mod generate_doubao_seedance_1p5_pro;
|
||||
pub mod generate_doubao_seedance_2p0;
|
||||
pub mod generate_doubao_seedance_2p0_fast;
|
||||
pub mod generate_vidu_q3;
|
||||
pub mod generate_vidu_q3_turbo;
|
||||
pub mod generate_wan_2p6;
|
||||
pub mod generate_wan_2p7;
|
||||
@@ -1,21 +0,0 @@
|
||||
//! comet_api_client
|
||||
//!
|
||||
//! HTTP client for CometAPI (https://www.cometapi.com/), which proxies a
|
||||
//! variety of third-party generative models. We use it for Doubao Seedance 2.0
|
||||
//! video generation ("Seedance 2.0" and "Seedance 2.0 Fast").
|
||||
//!
|
||||
//! Flow: enqueue with [`requests::create_video`], then poll with
|
||||
//! [`requests::get_video_task`] until the task reaches a terminal status.
|
||||
//!
|
||||
//! NB: There is deliberately no webhook module. CometAPI documents callbacks
|
||||
//! as provider-specific pass-through events with no standardized payload, and
|
||||
//! the Seedance video endpoint has no documented callback parameter. Comet's
|
||||
//! own guidance is to "keep polling as the source of truth".
|
||||
|
||||
pub mod creds;
|
||||
pub mod error;
|
||||
pub mod generate;
|
||||
pub mod requests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_utils;
|
||||
@@ -1,2 +0,0 @@
|
||||
/// Base URL for the CometAPI HTTP API.
|
||||
pub const COMET_API_BASE_URL: &str = "https://api.cometapi.com";
|
||||
@@ -1,439 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
use log::info;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
|
||||
use crate::creds::comet_api_key::CometApiKey;
|
||||
use crate::error::categorize_comet_error::categorize_create_video_error;
|
||||
use crate::error::comet_client_error::CometClientError;
|
||||
use crate::error::comet_error::CometError;
|
||||
use crate::error::comet_generic_api_error::CometGenericApiError;
|
||||
use crate::requests::comet_host::COMET_API_BASE_URL;
|
||||
use crate::requests::create_video::request_types::CreateVideoRawResponse;
|
||||
use crate::requests::video_task_status::CometVideoTaskStatus;
|
||||
|
||||
// ── Public args ──
|
||||
|
||||
/// Top-level argument to [`create_video`]. Borrows the API key separately
|
||||
/// from the request body so callers can log/save [`CreateVideoRequest`]
|
||||
/// without leaking the credential.
|
||||
pub struct CreateVideoArgs<'a> {
|
||||
pub api_key: &'a CometApiKey,
|
||||
pub request: CreateVideoRequest,
|
||||
}
|
||||
|
||||
/// The material part of a video creation request (`POST /v1/videos`).
|
||||
///
|
||||
/// This is the generic, model-agnostic wire shape. Per-model duration and
|
||||
/// size constraints are NOT enforced here — use the strongly-typed bindings
|
||||
/// in [`crate::generate::video`] which validate before delegating to this.
|
||||
#[derive(Clone)]
|
||||
pub struct CreateVideoRequest {
|
||||
/// Which video model fulfils the request.
|
||||
pub model: CometVideoModelRaw,
|
||||
|
||||
/// Text description of the desired video. Reference attached images in
|
||||
/// the prompt as `[Image 1]`, `[Image 2]`, etc.
|
||||
pub prompt: String,
|
||||
|
||||
/// Video duration in seconds. Valid ranges are model-specific. Default: 5.
|
||||
pub maybe_seconds: Option<u8>,
|
||||
|
||||
/// Output size: an aspect-ratio preset or exact dimensions.
|
||||
pub maybe_size: Option<CometVideoSize>,
|
||||
|
||||
/// Optional reference images (JPEG, PNG, WebP) for image-to-video.
|
||||
/// Each is sent as a repeated `input_reference` multipart field.
|
||||
/// Not all models support image input.
|
||||
pub input_reference_images: Vec<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
/// The video models CometAPI fronts on `POST /v1/videos`.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum CometVideoModelRaw {
|
||||
/// Seedance 2.0
|
||||
DoubaoSeedance2p0,
|
||||
/// Seedance 2.0 Fast
|
||||
DoubaoSeedance2p0Fast,
|
||||
/// Seedance 1.5 Pro (text-only)
|
||||
DoubaoSeedance1p5Pro,
|
||||
/// Seedance 1.0 Pro (text-only)
|
||||
DoubaoSeedance1p0Pro,
|
||||
/// Vidu Q3
|
||||
ViduQ3,
|
||||
/// Vidu Q3 Turbo
|
||||
ViduQ3Turbo,
|
||||
/// Alibaba Wan 2.7
|
||||
Wan2p7,
|
||||
/// Alibaba Wan 2.6
|
||||
Wan2p6,
|
||||
}
|
||||
|
||||
/// Output video size: a ratio preset or exact pixel dimensions.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum CometVideoSize {
|
||||
/// 16:9 (landscape)
|
||||
Landscape16x9,
|
||||
/// 4:3
|
||||
Standard4x3,
|
||||
/// 1:1 (square)
|
||||
Square,
|
||||
/// 3:4
|
||||
Portrait3x4,
|
||||
/// 9:16 (portrait)
|
||||
Portrait9x16,
|
||||
/// 21:9 (ultra-wide)
|
||||
UltraWide21x9,
|
||||
/// Exact dimensions, eg. 1280x720.
|
||||
Exact { width: u32, height: u32 },
|
||||
}
|
||||
|
||||
/// A reference image attached to the request as a binary multipart part.
|
||||
#[derive(Clone)]
|
||||
pub struct CometInputReferenceImage {
|
||||
/// Raw bytes of the image (JPEG, PNG, or WebP). Excluded from `Debug`
|
||||
/// output — a megabyte-scale blob shouldn't end up in a log line.
|
||||
pub file_bytes: Vec<u8>,
|
||||
|
||||
/// Filename for the multipart `Content-Disposition` header.
|
||||
pub filename: String,
|
||||
|
||||
/// Optional MIME type, eg. "image/png". If `None`, reqwest uses
|
||||
/// `application/octet-stream`.
|
||||
pub maybe_content_type: Option<String>,
|
||||
}
|
||||
|
||||
// ── Public response ──
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CreateVideoSuccess {
|
||||
/// Task identifier. Poll `GET /v1/videos/{task_id}` until terminal.
|
||||
pub task_id: String,
|
||||
|
||||
/// Initial lifecycle status, typically queued.
|
||||
pub status: CometVideoTaskStatus,
|
||||
|
||||
/// The model that will fulfil the task, as echoed by the API.
|
||||
pub maybe_model: Option<String>,
|
||||
|
||||
/// 0-100 completion percentage.
|
||||
pub maybe_progress: Option<u8>,
|
||||
|
||||
/// Unix timestamp (seconds).
|
||||
pub maybe_created_at: Option<i64>,
|
||||
}
|
||||
|
||||
/// Enqueue a Doubao Seedance 2.0 video generation task.
|
||||
///
|
||||
/// CometAPI's video creation endpoint takes `multipart/form-data`. The task
|
||||
/// id in the response is then polled with
|
||||
/// [`crate::requests::get_video_task::get_video_task::get_video_task`].
|
||||
pub async fn create_video(args: CreateVideoArgs<'_>) -> Result<CreateVideoSuccess, CometError> {
|
||||
let request = &args.request;
|
||||
request.validate()?;
|
||||
|
||||
let url = format!("{COMET_API_BASE_URL}/v1/videos");
|
||||
|
||||
let mut form = Form::new();
|
||||
for (field, value) in request.text_form_fields() {
|
||||
form = form.text(field, value);
|
||||
}
|
||||
for image in &request.input_reference_images {
|
||||
let mut part = Part::bytes(image.file_bytes.clone())
|
||||
.file_name(image.filename.clone());
|
||||
if let Some(content_type) = &image.maybe_content_type {
|
||||
part = part.mime_str(content_type)
|
||||
.map_err(|err| CometClientError::InvalidRequestField {
|
||||
field: "input_reference_images.maybe_content_type",
|
||||
raw_value: content_type.clone(),
|
||||
reason: format!("invalid mime type: {err}"),
|
||||
})?;
|
||||
}
|
||||
form = form.part("input_reference", part);
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.build()
|
||||
.map_err(CometClientError::ReqwestClientError)?;
|
||||
|
||||
let response = client.post(&url)
|
||||
.bearer_auth(args.api_key.as_str())
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await
|
||||
.map_err(CometGenericApiError::ReqwestError)?;
|
||||
|
||||
let status_code = response.status();
|
||||
let body = response.text()
|
||||
.await
|
||||
.map_err(CometGenericApiError::ReqwestError)?;
|
||||
|
||||
if !status_code.is_success() {
|
||||
return Err(categorize_create_video_error(status_code, body));
|
||||
}
|
||||
|
||||
let raw: CreateVideoRawResponse = serde_json::from_str(&body)
|
||||
.map_err(|err| CometGenericApiError::SerdeResponseParseErrorWithBody(err, body.clone()))?;
|
||||
|
||||
info!("Comet video task enqueued: {} (status: {})", raw.id, raw.status);
|
||||
|
||||
Ok(CreateVideoSuccess {
|
||||
task_id: raw.id,
|
||||
status: raw.status,
|
||||
maybe_model: raw.model,
|
||||
maybe_progress: raw.progress,
|
||||
maybe_created_at: raw.created_at,
|
||||
})
|
||||
}
|
||||
|
||||
impl CreateVideoRequest {
|
||||
/// The text fields of the multipart form, in wire order. Split out from
|
||||
/// [`create_video`] so the form shape is testable without HTTP.
|
||||
pub fn text_form_fields(&self) -> Vec<(&'static str, String)> {
|
||||
let mut fields = vec![
|
||||
("model", self.model.as_api_str().to_string()),
|
||||
("prompt", self.prompt.clone()),
|
||||
];
|
||||
if let Some(seconds) = self.maybe_seconds {
|
||||
fields.push(("seconds", seconds.to_string()));
|
||||
}
|
||||
if let Some(size) = self.maybe_size {
|
||||
fields.push(("size", size.as_api_string()));
|
||||
}
|
||||
fields
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<(), CometClientError> {
|
||||
if self.prompt.trim().is_empty() {
|
||||
return Err(CometClientError::InvalidRequestField {
|
||||
field: "prompt",
|
||||
raw_value: self.prompt.clone(),
|
||||
reason: "prompt must not be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CometVideoModelRaw {
|
||||
pub fn as_api_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::DoubaoSeedance2p0 => "doubao-seedance-2-0",
|
||||
Self::DoubaoSeedance2p0Fast => "doubao-seedance-2-0-fast",
|
||||
Self::DoubaoSeedance1p5Pro => "doubao-seedance-1-5-pro",
|
||||
Self::DoubaoSeedance1p0Pro => "doubao-seedance-1-0-pro",
|
||||
Self::ViduQ3 => "viduq3",
|
||||
Self::ViduQ3Turbo => "viduq3-turbo",
|
||||
Self::Wan2p7 => "wan2.7",
|
||||
Self::Wan2p6 => "wan2.6",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CometVideoSize {
|
||||
pub fn as_api_string(&self) -> String {
|
||||
match self {
|
||||
Self::Landscape16x9 => "16:9".to_string(),
|
||||
Self::Standard4x3 => "4:3".to_string(),
|
||||
Self::Square => "1:1".to_string(),
|
||||
Self::Portrait3x4 => "3:4".to_string(),
|
||||
Self::Portrait9x16 => "9:16".to_string(),
|
||||
Self::UltraWide21x9 => "21:9".to_string(),
|
||||
Self::Exact { width, height } => format!("{width}x{height}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for CreateVideoRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("CreateVideoRequest")
|
||||
.field("model", &self.model)
|
||||
.field("prompt", &self.prompt)
|
||||
.field("maybe_seconds", &self.maybe_seconds)
|
||||
.field("maybe_size", &self.maybe_size)
|
||||
.field("input_reference_images", &self.input_reference_images)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for CometInputReferenceImage {
|
||||
// Print byte-length, not raw bytes — derived Debug on Vec<u8> would dump
|
||||
// the entire image into the log line.
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("CometInputReferenceImage")
|
||||
.field("file_bytes_len", &self.file_bytes.len())
|
||||
.field("filename", &self.filename)
|
||||
.field("maybe_content_type", &self.maybe_content_type)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod shape_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_api_strings() {
|
||||
assert_eq!(CometVideoModelRaw::DoubaoSeedance2p0.as_api_str(), "doubao-seedance-2-0");
|
||||
assert_eq!(CometVideoModelRaw::DoubaoSeedance2p0Fast.as_api_str(), "doubao-seedance-2-0-fast");
|
||||
assert_eq!(CometVideoModelRaw::DoubaoSeedance1p5Pro.as_api_str(), "doubao-seedance-1-5-pro");
|
||||
assert_eq!(CometVideoModelRaw::DoubaoSeedance1p0Pro.as_api_str(), "doubao-seedance-1-0-pro");
|
||||
assert_eq!(CometVideoModelRaw::ViduQ3.as_api_str(), "viduq3");
|
||||
assert_eq!(CometVideoModelRaw::ViduQ3Turbo.as_api_str(), "viduq3-turbo");
|
||||
assert_eq!(CometVideoModelRaw::Wan2p7.as_api_str(), "wan2.7");
|
||||
assert_eq!(CometVideoModelRaw::Wan2p6.as_api_str(), "wan2.6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_api_strings() {
|
||||
assert_eq!(CometVideoSize::Landscape16x9.as_api_string(), "16:9");
|
||||
assert_eq!(CometVideoSize::Standard4x3.as_api_string(), "4:3");
|
||||
assert_eq!(CometVideoSize::Square.as_api_string(), "1:1");
|
||||
assert_eq!(CometVideoSize::Portrait3x4.as_api_string(), "3:4");
|
||||
assert_eq!(CometVideoSize::Portrait9x16.as_api_string(), "9:16");
|
||||
assert_eq!(CometVideoSize::UltraWide21x9.as_api_string(), "21:9");
|
||||
assert_eq!(CometVideoSize::Exact { width: 1280, height: 720 }.as_api_string(), "1280x720");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimal_form_fields() {
|
||||
let request = CreateVideoRequest {
|
||||
model: CometVideoModelRaw::DoubaoSeedance2p0Fast,
|
||||
prompt: "a corgi running through a field".to_string(),
|
||||
maybe_seconds: None,
|
||||
maybe_size: None,
|
||||
input_reference_images: vec![],
|
||||
};
|
||||
|
||||
assert_eq!(request.text_form_fields(), vec![
|
||||
("model", "doubao-seedance-2-0-fast".to_string()),
|
||||
("prompt", "a corgi running through a field".to_string()),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_form_fields() {
|
||||
let request = CreateVideoRequest {
|
||||
model: CometVideoModelRaw::DoubaoSeedance2p0,
|
||||
prompt: "make [Image 1] come alive".to_string(),
|
||||
maybe_seconds: Some(10),
|
||||
maybe_size: Some(CometVideoSize::Portrait9x16),
|
||||
input_reference_images: vec![],
|
||||
};
|
||||
|
||||
assert_eq!(request.text_form_fields(), vec![
|
||||
("model", "doubao-seedance-2-0".to_string()),
|
||||
("prompt", "make [Image 1] come alive".to_string()),
|
||||
("seconds", "10".to_string()),
|
||||
("size", "9:16".to_string()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
mod validation_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_prompt_is_rejected() {
|
||||
let request = request_with_prompt_and_seconds(" ", None);
|
||||
let error = request.validate().expect_err("should reject empty prompt");
|
||||
assert!(matches!(error, CometClientError::InvalidRequestField { field: "prompt", .. }));
|
||||
}
|
||||
|
||||
// NB: Duration ranges are model-specific and validated by the concrete
|
||||
// bindings in `generate::video`; the generic layer accepts any value.
|
||||
|
||||
fn request_with_prompt_and_seconds(prompt: &str, maybe_seconds: Option<u8>) -> CreateVideoRequest {
|
||||
CreateVideoRequest {
|
||||
model: CometVideoModelRaw::DoubaoSeedance2p0,
|
||||
prompt: prompt.to_string(),
|
||||
maybe_seconds,
|
||||
maybe_size: None,
|
||||
input_reference_images: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod live_tests {
|
||||
use super::*;
|
||||
use crate::test_utils::load_api_key;
|
||||
|
||||
/// Live test: enqueues a REAL generation and INCURS COSTS.
|
||||
/// Run manually with:
|
||||
/// cargo test -p comet_api_client live_create_video -- --ignored --nocapture
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn live_create_video() {
|
||||
let api_key = load_api_key();
|
||||
|
||||
let result = create_video(CreateVideoArgs {
|
||||
api_key: &api_key,
|
||||
request: CreateVideoRequest {
|
||||
model: CometVideoModelRaw::DoubaoSeedance2p0Fast,
|
||||
prompt: "a corgi running through a sunny meadow, cinematic".to_string(),
|
||||
maybe_seconds: Some(4),
|
||||
maybe_size: Some(CometVideoSize::Landscape16x9),
|
||||
input_reference_images: vec![],
|
||||
},
|
||||
}).await.expect("create_video should succeed");
|
||||
|
||||
println!("Enqueued task: {} (status: {})", result.task_id, result.status);
|
||||
assert!(!result.task_id.is_empty());
|
||||
}
|
||||
|
||||
/// Live test: enqueues a REAL generation and polls it to completion.
|
||||
/// INCURS COSTS and takes a few minutes. Run manually with:
|
||||
/// cargo test -p comet_api_client live_create_and_poll_video -- --ignored --nocapture
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn live_create_and_poll_video() {
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::requests::get_video_task::get_video_task::{get_video_task, GetVideoTaskArgs};
|
||||
|
||||
// NB: Comet's docs claim most tasks finish in 1-3 minutes, but tasks
|
||||
// have been observed to sit at `in_progress` well past 10 minutes.
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(15);
|
||||
const MAX_POLLS: u32 = 120; // ~30 minutes
|
||||
|
||||
let api_key = load_api_key();
|
||||
|
||||
let created = create_video(CreateVideoArgs {
|
||||
api_key: &api_key,
|
||||
request: CreateVideoRequest {
|
||||
model: CometVideoModelRaw::DoubaoSeedance2p0Fast,
|
||||
prompt: "a corgi running through a sunny meadow, cinematic".to_string(),
|
||||
maybe_seconds: Some(4),
|
||||
maybe_size: Some(CometVideoSize::Landscape16x9),
|
||||
input_reference_images: vec![],
|
||||
},
|
||||
}).await.expect("create_video should succeed");
|
||||
|
||||
println!("Enqueued task: {} (status: {})", created.task_id, created.status);
|
||||
|
||||
for poll in 1..=MAX_POLLS {
|
||||
tokio::time::sleep(POLL_INTERVAL).await;
|
||||
|
||||
let task = get_video_task(GetVideoTaskArgs {
|
||||
api_key: &api_key,
|
||||
task_id: &created.task_id,
|
||||
}).await.expect("get_video_task should succeed");
|
||||
|
||||
println!("Poll {}: status {} (progress: {:?})", poll, task.status, task.maybe_progress);
|
||||
|
||||
if task.status.is_terminal() {
|
||||
assert!(task.status.is_success(), "task should complete successfully: {:?}", task);
|
||||
let video_url = task.maybe_video_url.expect("completed task should have a video url");
|
||||
println!("Video URL: {}", video_url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Task {} did not reach a terminal state in time", created.task_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod create_video;
|
||||
pub mod request_types;
|
||||
@@ -1,77 +0,0 @@
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
use crate::requests::video_task_status::CometVideoTaskStatus;
|
||||
|
||||
/// Over-the-wire response from `POST /v1/videos`.
|
||||
///
|
||||
/// The docs show both `id` and `task_id` carrying the same task identifier;
|
||||
/// `task_id` is treated as an optional mirror of `id`.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct CreateVideoRawResponse {
|
||||
/// Task identifier, eg. "task_abc123". Use this with the poll endpoint.
|
||||
pub id: String,
|
||||
|
||||
/// Mirror of `id`.
|
||||
pub task_id: Option<String>,
|
||||
|
||||
/// Always "video".
|
||||
pub object: Option<String>,
|
||||
|
||||
/// The model that will fulfil the task, eg. "doubao-seedance-2-0".
|
||||
pub model: Option<String>,
|
||||
|
||||
/// Initial lifecycle status, typically "queued".
|
||||
pub status: CometVideoTaskStatus,
|
||||
|
||||
/// 0-100 completion percentage.
|
||||
pub progress: Option<u8>,
|
||||
|
||||
/// Unix timestamp (seconds).
|
||||
pub created_at: Option<i64>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Documented response shape from
|
||||
/// https://apidoc.cometapi.com/api/video/seedance/create.md
|
||||
const REAL_CREATE_RESPONSE: &str = r#"
|
||||
{
|
||||
"id": "task_abc123",
|
||||
"task_id": "task_abc123",
|
||||
"object": "video",
|
||||
"model": "doubao-seedance-2-0",
|
||||
"status": "queued",
|
||||
"progress": 0,
|
||||
"created_at": 1776681149
|
||||
}
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn parse_real_create_response() {
|
||||
let response: CreateVideoRawResponse = serde_json::from_str(REAL_CREATE_RESPONSE)
|
||||
.expect("should parse");
|
||||
|
||||
assert_eq!(response.id, "task_abc123");
|
||||
assert_eq!(response.task_id.as_deref(), Some("task_abc123"));
|
||||
assert_eq!(response.object.as_deref(), Some("video"));
|
||||
assert_eq!(response.model.as_deref(), Some("doubao-seedance-2-0"));
|
||||
assert_eq!(response.status, CometVideoTaskStatus::Queued);
|
||||
assert_eq!(response.progress, Some(0));
|
||||
assert_eq!(response.created_at, Some(1776681149));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_minimal_response() {
|
||||
// Only `id` and `status` are load-bearing; everything else is optional
|
||||
// so schema drift doesn't break enqueuing.
|
||||
let response: CreateVideoRawResponse = serde_json::from_str(
|
||||
r#"{"id":"task_xyz","status":"queued"}"#,
|
||||
).expect("should parse");
|
||||
|
||||
assert_eq!(response.id, "task_xyz");
|
||||
assert_eq!(response.status, CometVideoTaskStatus::Queued);
|
||||
assert_eq!(response.task_id, None);
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
use log::info;
|
||||
|
||||
use crate::creds::comet_api_key::CometApiKey;
|
||||
use crate::error::categorize_comet_error::categorize_get_video_task_error;
|
||||
use crate::error::comet_client_error::CometClientError;
|
||||
use crate::error::comet_error::CometError;
|
||||
use crate::error::comet_generic_api_error::CometGenericApiError;
|
||||
use crate::requests::comet_host::COMET_API_BASE_URL;
|
||||
use crate::requests::get_video_task::request_types::VideoTaskRawResponse;
|
||||
use crate::requests::video_task_status::CometVideoTaskStatus;
|
||||
|
||||
// ── Public args ──
|
||||
|
||||
pub struct GetVideoTaskArgs<'a> {
|
||||
pub api_key: &'a CometApiKey,
|
||||
|
||||
/// The task id returned by the creation endpoint, eg. "task_abc123".
|
||||
pub task_id: &'a str,
|
||||
}
|
||||
|
||||
// ── Public response ──
|
||||
|
||||
/// Snapshot of a video task's state.
|
||||
///
|
||||
/// Poll every 10-20 seconds until [`CometVideoTaskStatus::is_terminal`];
|
||||
/// most tasks finish within 1-3 minutes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CometVideoTask {
|
||||
pub task_id: String,
|
||||
|
||||
pub status: CometVideoTaskStatus,
|
||||
|
||||
/// The Seedance model used, as echoed by the API.
|
||||
pub maybe_model: Option<String>,
|
||||
|
||||
/// 0-100 completion percentage.
|
||||
pub maybe_progress: Option<u8>,
|
||||
|
||||
/// Signed download URL, present only when the task completed. The
|
||||
/// signature is time-limited — download or re-host the file promptly.
|
||||
pub maybe_video_url: Option<String>,
|
||||
|
||||
/// Unix timestamp (seconds).
|
||||
pub maybe_created_at: Option<i64>,
|
||||
|
||||
/// Unix timestamp (seconds) when the task finished.
|
||||
pub maybe_completed_at: Option<i64>,
|
||||
}
|
||||
|
||||
/// Fetch the current state of a video task (`GET /v1/videos/{id}`).
|
||||
pub async fn get_video_task(args: GetVideoTaskArgs<'_>) -> Result<CometVideoTask, CometError> {
|
||||
let url = format!("{COMET_API_BASE_URL}/v1/videos/{}", args.task_id);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.build()
|
||||
.map_err(CometClientError::ReqwestClientError)?;
|
||||
|
||||
let response = client.get(&url)
|
||||
.bearer_auth(args.api_key.as_str())
|
||||
.send()
|
||||
.await
|
||||
.map_err(CometGenericApiError::ReqwestError)?;
|
||||
|
||||
let status_code = response.status();
|
||||
let body = response.text()
|
||||
.await
|
||||
.map_err(CometGenericApiError::ReqwestError)?;
|
||||
|
||||
if !status_code.is_success() {
|
||||
return Err(categorize_get_video_task_error(status_code, body, args.task_id));
|
||||
}
|
||||
|
||||
let raw: VideoTaskRawResponse = serde_json::from_str(&body)
|
||||
.map_err(|err| CometGenericApiError::SerdeResponseParseErrorWithBody(err, body.clone()))?;
|
||||
|
||||
info!("Comet video task {}: status {} (progress: {:?})", raw.id, raw.status, raw.progress);
|
||||
|
||||
Ok(CometVideoTask {
|
||||
task_id: raw.id,
|
||||
status: raw.status,
|
||||
maybe_model: raw.model,
|
||||
maybe_progress: raw.progress,
|
||||
maybe_video_url: raw.video_url,
|
||||
maybe_created_at: raw.created_at,
|
||||
maybe_completed_at: raw.completed_at,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::comet_specific_api_error::CometSpecificApiError;
|
||||
use crate::requests::get_video_content::get_video_content::{
|
||||
probe_video_failure_reason, ProbeVideoFailureReasonArgs,
|
||||
};
|
||||
use crate::test_utils::load_api_key;
|
||||
|
||||
/// Historical task ids from real generations, kept for diagnosing how
|
||||
/// CometAPI reports terminal states. Several of these failed content
|
||||
/// moderation ("VIOLATION").
|
||||
const KNOWN_TASK_IDS: &[&str] = &[
|
||||
"task_ZISEWaXgmyqXWiesd287qTyAZLapbwz8",
|
||||
"task_ZvwV2nhp08ng4krT16CPxtHZ2SgABElj",
|
||||
"task_GZPVhcnIZKLJBqsCGTBvMiUFN9uDnwJv",
|
||||
"task_iGnAQ5lwqhMltxhN5k9yATAjs7AgjV39",
|
||||
"task_hGhShys2Wi2gPf2qUkuQ38VKIytWAw1d",
|
||||
"task_t84NmrcT3k8s6Il4xJeKtfxXipA3n8RQ",
|
||||
"task_6OhdGVFZjHNtOKKx8Ewz37mENaVIhC3X",
|
||||
];
|
||||
|
||||
/// Live test: makes a REAL network request (no generation cost — the task
|
||||
/// id is bogus, so this exercises auth + the TaskNotFound error path).
|
||||
/// Run manually with:
|
||||
/// cargo test -p comet_api_client live_get_video_task -- --ignored --nocapture
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn live_get_video_task_unknown_id() {
|
||||
let api_key = load_api_key();
|
||||
|
||||
let result = get_video_task(GetVideoTaskArgs {
|
||||
api_key: &api_key,
|
||||
task_id: "task_does_not_exist_artcraft_test",
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
Err(CometError::ApiSpecific(CometSpecificApiError::TaskNotFound { task_id, .. })) => {
|
||||
assert_eq!(task_id, "task_does_not_exist_artcraft_test");
|
||||
}
|
||||
other => panic!("Expected TaskNotFound, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Live diagnostic: polls the known historical tasks and prints their full
|
||||
/// state, including the failure reason recovered from the content
|
||||
/// endpoint for failed tasks. Free (read-only GETs, no generation cost),
|
||||
/// but hits the real API. Run manually with:
|
||||
/// cargo test -p comet_api_client live_poll_known_task_ids -- --ignored --nocapture
|
||||
///
|
||||
/// NB: The poll endpoint reports failures as a bare `status: "failed"`.
|
||||
/// The ONLY way to learn WHY is `GET /v1/videos/{id}/content`, whose error
|
||||
/// message leaks the underlying provider status (eg. "VIOLATION" for
|
||||
/// content-moderation rejections — these run to progress 100 and then
|
||||
/// fail when the finished output is moderated).
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn live_poll_known_task_ids() {
|
||||
let api_key = load_api_key();
|
||||
|
||||
for task_id in KNOWN_TASK_IDS {
|
||||
println!("===== {task_id}");
|
||||
|
||||
let task = match get_video_task(GetVideoTaskArgs { api_key: &api_key, task_id }).await {
|
||||
Ok(task) => task,
|
||||
Err(err) => {
|
||||
println!(" poll error: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
println!(" status: {}", task.status);
|
||||
println!(" model: {}", task.maybe_model.as_deref().unwrap_or("?"));
|
||||
println!(" progress: {:?}", task.maybe_progress);
|
||||
println!(" created_at: {:?}", task.maybe_created_at);
|
||||
println!(" completed_at: {:?}", task.maybe_completed_at);
|
||||
println!(" video_url: {}", task.maybe_video_url.as_deref().unwrap_or("(none)"));
|
||||
|
||||
if task.status.is_failure() {
|
||||
match probe_video_failure_reason(ProbeVideoFailureReasonArgs { api_key: &api_key, task_id }).await {
|
||||
Ok(Some(reason)) => {
|
||||
println!(" failure reason (via /content):");
|
||||
println!(" raw message: {}", reason.raw_message);
|
||||
println!(" underlying status: {}", reason.maybe_underlying_status.as_deref().unwrap_or("(unparsed)"));
|
||||
println!(" content violation: {}", reason.is_content_violation());
|
||||
println!(" error type: {}", reason.maybe_error_type.as_deref().unwrap_or("(none)"));
|
||||
println!(" request id: {}", reason.maybe_request_id.as_deref().unwrap_or("(none)"));
|
||||
}
|
||||
Ok(None) => println!(" failure probe: content unexpectedly downloadable"),
|
||||
Err(err) => println!(" failure probe error: {err}"),
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod get_video_task;
|
||||
pub mod request_types;
|
||||
@@ -1,113 +0,0 @@
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
use crate::requests::video_task_status::CometVideoTaskStatus;
|
||||
|
||||
/// Over-the-wire response from `GET /v1/videos/{id}`.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct VideoTaskRawResponse {
|
||||
/// Task identifier.
|
||||
pub id: String,
|
||||
|
||||
/// Always "video".
|
||||
pub object: Option<String>,
|
||||
|
||||
/// The Seedance model used, eg. "doubao-seedance-2-0".
|
||||
pub model: Option<String>,
|
||||
|
||||
/// Lifecycle status.
|
||||
pub status: CometVideoTaskStatus,
|
||||
|
||||
/// 0-100 completion percentage.
|
||||
pub progress: Option<u8>,
|
||||
|
||||
/// Signed download URL. Present only when `status` is "completed".
|
||||
/// NB: the signature is time-limited — download or re-host the file
|
||||
/// before it expires.
|
||||
pub video_url: Option<String>,
|
||||
|
||||
/// Unix timestamp (seconds).
|
||||
pub created_at: Option<i64>,
|
||||
|
||||
/// Unix timestamp (seconds) when the task finished.
|
||||
pub completed_at: Option<i64>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Documented response shapes from
|
||||
// https://apidoc.cometapi.com/api/video/seedance/query.md
|
||||
|
||||
const REAL_IN_PROGRESS_RESPONSE: &str = r#"
|
||||
{
|
||||
"id": "task_abc123",
|
||||
"object": "video",
|
||||
"model": "doubao-seedance-2-0",
|
||||
"status": "in_progress",
|
||||
"progress": 30,
|
||||
"created_at": 1777385418,
|
||||
"completed_at": 1777385485
|
||||
}
|
||||
"#;
|
||||
|
||||
const REAL_COMPLETED_RESPONSE: &str = r#"
|
||||
{
|
||||
"id": "task_abc123",
|
||||
"object": "video",
|
||||
"model": "doubao-seedance-2-0",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"created_at": 1777385418,
|
||||
"completed_at": 1777385526,
|
||||
"video_url": "https://example.com/seedance-output.mp4"
|
||||
}
|
||||
"#;
|
||||
|
||||
const REAL_FAILED_RESPONSE: &str = r#"
|
||||
{
|
||||
"id": "task_abc123",
|
||||
"object": "video",
|
||||
"model": "doubao-seedance-2-0",
|
||||
"status": "failed",
|
||||
"progress": 0,
|
||||
"created_at": 1777385418,
|
||||
"completed_at": 1777385526
|
||||
}
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn parse_real_in_progress_response() {
|
||||
let response: VideoTaskRawResponse = serde_json::from_str(REAL_IN_PROGRESS_RESPONSE)
|
||||
.expect("should parse");
|
||||
|
||||
assert_eq!(response.id, "task_abc123");
|
||||
assert_eq!(response.status, CometVideoTaskStatus::InProgress);
|
||||
assert_eq!(response.progress, Some(30));
|
||||
assert_eq!(response.video_url, None);
|
||||
assert!(!response.status.is_terminal());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_real_completed_response() {
|
||||
let response: VideoTaskRawResponse = serde_json::from_str(REAL_COMPLETED_RESPONSE)
|
||||
.expect("should parse");
|
||||
|
||||
assert_eq!(response.status, CometVideoTaskStatus::Completed);
|
||||
assert_eq!(response.progress, Some(100));
|
||||
assert_eq!(response.video_url.as_deref(), Some("https://example.com/seedance-output.mp4"));
|
||||
assert_eq!(response.completed_at, Some(1777385526));
|
||||
assert!(response.status.is_success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_real_failed_response() {
|
||||
let response: VideoTaskRawResponse = serde_json::from_str(REAL_FAILED_RESPONSE)
|
||||
.expect("should parse");
|
||||
|
||||
assert_eq!(response.status, CometVideoTaskStatus::Failed);
|
||||
assert_eq!(response.video_url, None);
|
||||
assert!(response.status.is_terminal());
|
||||
assert!(response.status.is_failure());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
pub mod comet_host;
|
||||
pub mod create_video;
|
||||
pub mod get_video_content;
|
||||
pub mod get_video_task;
|
||||
pub mod video_task_status;
|
||||
@@ -1,132 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
use serde::de::{Deserialize, Deserializer};
|
||||
use serde::ser::{Serialize, Serializer};
|
||||
|
||||
/// Lifecycle status of a CometAPI video task.
|
||||
///
|
||||
/// The `Unknown(String)` catch-all future-proofs against CometAPI adding new
|
||||
/// states — callers should treat unknown states as still-running unless
|
||||
/// proven otherwise.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum CometVideoTaskStatus {
|
||||
Queued,
|
||||
InProgress,
|
||||
Completed,
|
||||
Failed,
|
||||
Error,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl CometVideoTaskStatus {
|
||||
pub fn to_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Queued => "queued",
|
||||
Self::InProgress => "in_progress",
|
||||
Self::Completed => "completed",
|
||||
Self::Failed => "failed",
|
||||
Self::Error => "error",
|
||||
Self::Unknown(value) => value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Self {
|
||||
match value {
|
||||
"queued" => Self::Queued,
|
||||
"in_progress" => Self::InProgress,
|
||||
"completed" => Self::Completed,
|
||||
"failed" => Self::Failed,
|
||||
"error" => Self::Error,
|
||||
other => Self::Unknown(other.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the task has reached a terminal state (stop polling).
|
||||
pub fn is_terminal(&self) -> bool {
|
||||
matches!(self, Self::Completed | Self::Failed | Self::Error)
|
||||
}
|
||||
|
||||
pub fn is_success(&self) -> bool {
|
||||
matches!(self, Self::Completed)
|
||||
}
|
||||
|
||||
pub fn is_failure(&self) -> bool {
|
||||
matches!(self, Self::Failed | Self::Error)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CometVideoTaskStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.to_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CometVideoTaskStatus {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let value = String::deserialize(deserializer)?;
|
||||
Ok(Self::from_str(&value))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for CometVideoTaskStatus {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(self.to_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deserialize_known_states() {
|
||||
let cases = [
|
||||
("\"queued\"", CometVideoTaskStatus::Queued),
|
||||
("\"in_progress\"", CometVideoTaskStatus::InProgress),
|
||||
("\"completed\"", CometVideoTaskStatus::Completed),
|
||||
("\"failed\"", CometVideoTaskStatus::Failed),
|
||||
("\"error\"", CometVideoTaskStatus::Error),
|
||||
];
|
||||
for (json, expected) in cases {
|
||||
let status: CometVideoTaskStatus = serde_json::from_str(json).expect("should parse");
|
||||
assert_eq!(status, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_unknown_state_is_future_proof() {
|
||||
let status: CometVideoTaskStatus = serde_json::from_str("\"hyper_queued\"").expect("should parse");
|
||||
assert_eq!(status, CometVideoTaskStatus::Unknown("hyper_queued".to_string()));
|
||||
assert!(!status.is_terminal());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for status in [
|
||||
CometVideoTaskStatus::Queued,
|
||||
CometVideoTaskStatus::InProgress,
|
||||
CometVideoTaskStatus::Completed,
|
||||
CometVideoTaskStatus::Failed,
|
||||
CometVideoTaskStatus::Error,
|
||||
CometVideoTaskStatus::Unknown("new_state".to_string()),
|
||||
] {
|
||||
let json = serde_json::to_string(&status).expect("should serialize");
|
||||
let parsed: CometVideoTaskStatus = serde_json::from_str(&json).expect("should parse");
|
||||
assert_eq!(parsed, status);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_states() {
|
||||
assert!(!CometVideoTaskStatus::Queued.is_terminal());
|
||||
assert!(!CometVideoTaskStatus::InProgress.is_terminal());
|
||||
assert!(CometVideoTaskStatus::Completed.is_terminal());
|
||||
assert!(CometVideoTaskStatus::Failed.is_terminal());
|
||||
assert!(CometVideoTaskStatus::Error.is_terminal());
|
||||
|
||||
assert!(CometVideoTaskStatus::Completed.is_success());
|
||||
assert!(!CometVideoTaskStatus::Completed.is_failure());
|
||||
assert!(CometVideoTaskStatus::Failed.is_failure());
|
||||
assert!(CometVideoTaskStatus::Error.is_failure());
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
use std::fs::read_to_string;
|
||||
|
||||
use crate::creds::comet_api_key::CometApiKey;
|
||||
|
||||
const API_KEY_PATH: &str = "/Users/bt/Artcraft/credentials/comet_api_key.txt";
|
||||
|
||||
pub fn load_api_key() -> CometApiKey {
|
||||
let secret = read_to_string(API_KEY_PATH)
|
||||
.expect("Failed to read Comet API key file");
|
||||
CometApiKey::from_str(&secret)
|
||||
}
|
||||
+1
-2
@@ -87,8 +87,7 @@ mod tests {
|
||||
use crate::requests::image_websocket::messages::websocket_server_message::WebsocketServerMessage;
|
||||
|
||||
fn json_path(file_name: &str) -> String {
|
||||
// NB: Cargo runs tests with the crate root as the working directory.
|
||||
format!("test_data/websocket_messages/{}", file_name)
|
||||
format!("/Users/bt/dev/storyteller/storyteller-rust/crates/api_clients/grok_consumer_client/test_data/websocket_messages/{}", file_name)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
-18
File diff suppressed because one or more lines are too long
-25
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"type": "json",
|
||||
"current_status": "completed",
|
||||
"percentage_complete": 100,
|
||||
"job_id": "c1bed796-3f05-4716-8ef0-a4d4900dfc6e",
|
||||
"elapsed": null,
|
||||
"prompt": "a dog typing on a keyboard",
|
||||
"confirmation": "Drew a dog typing",
|
||||
"full_prompt": "a dog typing on a keyboard",
|
||||
"flags": null,
|
||||
"is_alteration": false,
|
||||
"section_id": 0,
|
||||
"input_type": "text",
|
||||
"request_id": "fc39cefd-fcf9-4775-b490-7e17ce670060",
|
||||
"asr_transcript": null,
|
||||
"asr_accepted": null,
|
||||
"seed": null,
|
||||
"image_reference": null,
|
||||
"is_image_edit": null,
|
||||
"moderated": false,
|
||||
"r_rated": false,
|
||||
"order": 1,
|
||||
"model_name": "imagine_h_1",
|
||||
"image_id": "78b3e6ef-defc-41a6-a9ac-b7a370d26ce5"
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"type": "json",
|
||||
"current_status": "start_stage",
|
||||
"percentage_complete": 0,
|
||||
"job_id": "57baeffe-d5cf-483b-a5bb-7794899f905c",
|
||||
"elapsed": null,
|
||||
"prompt": "a dog typing on a keyboard",
|
||||
"confirmation": "Drew a dog typing",
|
||||
"full_prompt": "a dog typing on a keyboard",
|
||||
"flags": null,
|
||||
"is_alteration": true,
|
||||
"section_id": 0,
|
||||
"input_type": "text",
|
||||
"request_id": "fc39cefd-fcf9-4775-b490-7e17ce670060",
|
||||
"asr_transcript": null,
|
||||
"asr_accepted": null,
|
||||
"seed": null,
|
||||
"image_reference": null,
|
||||
"is_image_edit": null,
|
||||
"moderated": false,
|
||||
"r_rated": null,
|
||||
"order": 7,
|
||||
"model_name": "imagine_h_1",
|
||||
"image_id": null
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"type": "conversation.item.create",
|
||||
"timestamp": 1761010203630,
|
||||
"item": {
|
||||
"type": "message",
|
||||
"content": [
|
||||
{
|
||||
"requestId": "fc39cefd-fcf9-4775-b490-7e17ce670060",
|
||||
"text": "a dog typing on a keyboard",
|
||||
"type": "input_text",
|
||||
"properties": {
|
||||
"section_count": 0,
|
||||
"is_kids_mode": false,
|
||||
"enable_nsfw": true,
|
||||
"skip_upsampler": false,
|
||||
"is_initial": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"type": "other_type",
|
||||
"foo": "bar"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"type":"json","current_status":"start_stage","percentage_complete":0.0,"job_id":"0080eaac-f7f7-4369-90a2-346491939c1b","elapsed":null,"prompt":"a dog riding a motorcycle","confirmation":"Drew a dog riding a motorcycle","full_prompt":"a dog riding a motorcycle","flags":null,"is_alteration":false,"section_id":0,"input_type":"text","request_id":"bbc15402-3257-4415-922e-b156c151f3c2","asr_transcript":null,"asr_accepted":null,"seed":null,"image_reference":null,"is_image_edit":null,"moderated":false,"r_rated":null,"order":0,"model_name":"imagine_h_1","image_id":null}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"type": "json",
|
||||
"current_status": "start_stage",
|
||||
"percentage_complete": 0,
|
||||
"job_id": "0080eaac-f7f7-4369-90a2-346491939c1b",
|
||||
"elapsed": null,
|
||||
"prompt": "a dog riding a motorcycle",
|
||||
"confirmation": "Drew a dog riding a motorcycle",
|
||||
"full_prompt": "a dog riding a motorcycle",
|
||||
"flags": null,
|
||||
"is_alteration": false,
|
||||
"section_id": 0,
|
||||
"input_type": "text",
|
||||
"request_id": "bbc15402-3257-4415-922e-b156c151f3c2",
|
||||
"asr_transcript": null,
|
||||
"asr_accepted": null,
|
||||
"seed": null,
|
||||
"image_reference": null,
|
||||
"is_image_edit": null,
|
||||
"moderated": false,
|
||||
"r_rated": null,
|
||||
"order": 0,
|
||||
"model_name": "imagine_h_1",
|
||||
"image_id": null
|
||||
}
|
||||
@@ -89,15 +89,7 @@ mod tests {
|
||||
let cookies = format!("cookie={}", "a".repeat(10000));
|
||||
assert_eq!(has_session_cookie(&cookies).unwrap(), SessionCookiePresence::MaybePresent);
|
||||
|
||||
// Synthetic fixture (the original real session token was redacted); the session
|
||||
// cookie name is what matters for detection.
|
||||
let cookies = "__Host-authjs.csrf-token=abc123; \
|
||||
__Secure-authjs.callback-url=https%3A%2F%2Fsora.com%2F; \
|
||||
__Secure-authjs.session-token=fake-session-token-value";
|
||||
|
||||
assert_eq!(has_session_cookie(cookies).unwrap(), SessionCookiePresence::Present);
|
||||
|
||||
let cookies = "__Secure-next-auth.session-token=fake-session-token-value";
|
||||
let cookies = "";
|
||||
|
||||
assert_eq!(has_session_cookie(cookies).unwrap(), SessionCookiePresence::Present);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod seedance2pro_bad_request_api_error;
|
||||
pub mod seedance2pro_client_error;
|
||||
pub mod seedance2pro_error;
|
||||
pub mod seedance2pro_generic_api_error;
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// These represent known "400s" that are the fault of the client sending a bad request.
|
||||
#[derive(Debug)]
|
||||
pub enum Seedance2ProBadRequestApiError {
|
||||
/// The video generation request was flagged as a content violation.
|
||||
VideoGenerationViolation { raw_body: String },
|
||||
|
||||
/// The prompt exceeded Kinovi's maximum length (10,000 characters at the
|
||||
/// time of writing).
|
||||
PromptIsTooLong { raw_body: String },
|
||||
|
||||
/// Too many media URLs were attached to the request (`uploadedUrls` is
|
||||
/// capped at 9 elements at the time of writing).
|
||||
TooManyUrls { raw_body: String },
|
||||
}
|
||||
|
||||
impl Error for Seedance2ProBadRequestApiError {}
|
||||
|
||||
impl Display for Seedance2ProBadRequestApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::VideoGenerationViolation { raw_body } => write!(f, "Video generation violation: {}", raw_body),
|
||||
Self::PromptIsTooLong { raw_body } => write!(f, "Prompt is too long: {}", raw_body),
|
||||
Self::TooManyUrls { raw_body } => write!(f, "Too many urls: {}", raw_body),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
@@ -10,7 +9,6 @@ pub enum Seedance2ProError {
|
||||
Client(Seedance2ProClientError),
|
||||
ApiSpecific(Seedance2ProSpecificApiError),
|
||||
ApiGeneric(Seedance2ProGenericApiError),
|
||||
ApiBadRequest(Seedance2ProBadRequestApiError),
|
||||
}
|
||||
|
||||
impl Seedance2ProError {
|
||||
@@ -40,7 +38,6 @@ impl std::fmt::Display for Seedance2ProError {
|
||||
Self::Client(e) => write!(f, "Seedance2ProClientError: {:?}", e),
|
||||
Self::ApiSpecific(e) => write!(f, "Seedance2ProSpecificApiError: {:?}", e),
|
||||
Self::ApiGeneric(e) => write!(f, "Seedance2ProGenericApiError: {:?}", e),
|
||||
Self::ApiBadRequest(e) => write!(f, "Seedance2ProBadRequestApiError: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,9 +59,3 @@ impl From<Seedance2ProGenericApiError> for Seedance2ProError {
|
||||
Self::ApiGeneric(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Seedance2ProBadRequestApiError> for Seedance2ProError {
|
||||
fn from(error: Seedance2ProBadRequestApiError) -> Self {
|
||||
Self::ApiBadRequest(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,8 @@ pub enum Seedance2ProGenericApiError {
|
||||
/// Includes the original body.
|
||||
SerdeParseErrorWithBodyOnNon200(serde_json::Error, String),
|
||||
|
||||
/// A response that parsed, but didn't have the shape we expected
|
||||
/// (eg. an empty batch response array).
|
||||
UnexpectedResponseShape {
|
||||
explanation: String,
|
||||
raw_body: String,
|
||||
},
|
||||
/// An uncategorized bad HTTP response.
|
||||
UncategorizedBadResponse(String),
|
||||
|
||||
/// An uncategorized bad HTTP response with status code and body.
|
||||
UncategorizedBadResponseWithStatusAndBody {
|
||||
@@ -41,7 +37,7 @@ impl Display for Seedance2ProGenericApiError {
|
||||
Self::CloudflareError(err) => write!(f, "Cloudflare error: {}", err),
|
||||
Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body),
|
||||
Self::SerdeParseErrorWithBodyOnNon200(err, body) => write!(f, "Failed to parse non-200 response body: {:?}. Body: {}", err, body),
|
||||
Self::UnexpectedResponseShape { explanation, raw_body } => write!(f, "Unexpected response shape: {}. Body: {}", explanation, raw_body),
|
||||
Self::UncategorizedBadResponse(msg) => write!(f, "Uncategorized bad response: {}", msg),
|
||||
Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body),
|
||||
Self::WreqError(err) => write!(f, "Wreq client error: {}", err),
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ pub enum Seedance2ProSpecificApiError {
|
||||
/// The session cookies are expired or invalid.
|
||||
UnauthorizedSessionExpired,
|
||||
|
||||
/// The video generation request was flagged as a content violation.
|
||||
VideoGenerationViolation(String),
|
||||
|
||||
/// The account does not have enough credits to perform the operation.
|
||||
BillingError {
|
||||
status_code: StatusCode,
|
||||
@@ -21,6 +24,7 @@ impl Display for Seedance2ProSpecificApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::UnauthorizedSessionExpired => write!(f, "Unauthorized: session cookies expired or invalid."),
|
||||
Self::VideoGenerationViolation(body) => write!(f, "Video generation violation: {}", body),
|
||||
Self::BillingError { status_code, body } => write!(f, "Billing error (status {}): {}", status_code, body),
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -119,10 +119,9 @@ pub async fn generate_character(args: GenerateCharacterArgs<'_>) -> Result<Gener
|
||||
let data = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
|
||||
explanation: "Empty batch response array".to_string(),
|
||||
raw_body: response_body.clone(),
|
||||
})?
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
+5
-6
@@ -1,8 +1,8 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
use crate::requests::generate_image::request_types::*;
|
||||
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
|
||||
use crate::utils::categorize_seedance2pro_error::categorize_seedance2pro_error;
|
||||
@@ -285,16 +285,15 @@ pub async fn generate_image(args: GenerateImageArgs<'_>) -> Result<GenerateImage
|
||||
let task_data = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
|
||||
explanation: "Empty batch response array".to_string(),
|
||||
raw_body: response_body.clone(),
|
||||
})?
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
if task_data.violation_warning {
|
||||
return Err(Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: response_body }.into());
|
||||
return Err(Seedance2ProSpecificApiError::VideoGenerationViolation(response_body).into());
|
||||
}
|
||||
|
||||
Ok(GenerateImageResponse {
|
||||
|
||||
+5
-6
@@ -1,8 +1,8 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
use crate::requests::generate_video::request_types::*;
|
||||
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
|
||||
use crate::utils::categorize_seedance2pro_error::categorize_seedance2pro_error;
|
||||
@@ -403,16 +403,15 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
|
||||
let task_data = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
|
||||
explanation: "Empty batch response array".to_string(),
|
||||
raw_body: response_body.clone(),
|
||||
})?
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
if task_data.violation_warning {
|
||||
return Err(Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: response_body }.into());
|
||||
return Err(Seedance2ProSpecificApiError::VideoGenerationViolation(response_body).into());
|
||||
}
|
||||
|
||||
Ok(GenerateVideoResponse {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user