mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-24 18:59:28 -04:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32248cb540 | ||
|
|
3f93fa835e | ||
|
|
04a5637dc0 | ||
|
|
b4e1e3ea57 | ||
|
|
71fd694368 | ||
|
|
f2c386dcb9 | ||
|
|
ffb8cbe433 | ||
|
|
5f062c4512 | ||
|
|
2442cda22f | ||
|
|
0a534921ce | ||
|
|
1fecc4405c | ||
|
|
cbca7e3cb4 | ||
|
|
d107b78498 | ||
|
|
702bdc8777 | ||
|
|
4fe36781de | ||
|
|
029375052d | ||
|
|
72d680411f | ||
|
|
79a7443baa | ||
|
|
004a5c157a | ||
|
|
63d3d63709 | ||
|
|
d6e23838ce | ||
|
|
6785db1d59 |
@@ -20,10 +20,13 @@ then the story, then the fine print.
|
||||
### Ordering within a file
|
||||
|
||||
1. **Constants** at the top, right after imports.
|
||||
2. **Structs, enums, and type definitions** next. If a struct contains another struct, place
|
||||
the outer/larger struct above the inner/smaller one.
|
||||
3. **Type ordering for API types**: Request, then Response (success), then Error.
|
||||
4. **`impl` blocks** after their type definitions.
|
||||
2. **The primary public type** (the main struct/enum the file is about) comes next.
|
||||
A reader opening this file should immediately see what it defines.
|
||||
3. **Supporting/subordinate types** (sub-structs, helper enums, serialization DTOs) go
|
||||
below the primary type, not above it. Ask: "Would a reader need to know this type
|
||||
exists before seeing the main type?" — if no, push it down.
|
||||
4. **Type ordering for API types**: Request, then Response (success), then Error.
|
||||
5. **`impl` blocks** after their type definitions.
|
||||
|
||||
### Ordering within an `impl` block
|
||||
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO characters\nSET\n token = ?,\n character_type = ?,\n is_active = false,\n character_name = ?,\n maybe_description = ?,\n maybe_avatar_media_token = NULL,\n maybe_full_image_media_token = NULL,\n maybe_original_upload_media_token = ?,\n maybe_creator_user_token = ?,\n creator_ip_address = ?,\n kinovi_character_id = ?,\n kinovi_character_name = ?,\n maybe_kinovi_asset_id = NULL,\n maybe_generic_inference_job_token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 10
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2a19cc533815aa3325b940eafe56b9f014fedb3b360b954935d6ec13ff14302d"
|
||||
}
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO users\nSET\n token = ?,\n \n is_temporary = ?,\n \n username = ?,\n display_name = ?,\n\n username_is_generated = ?,\n username_is_not_customized = ?,\n\n email_address = ?,\n email_gravatar_hash = ?,\n\n email_confirmed = FALSE,\n email_confirmed_by_google = ?,\n email_is_synthetic = ?,\n \n was_eagerly_provisioned = ?,\n\n profile_markdown = ?,\n profile_rendered_html = ?,\n user_role_slug = ?,\n\n password_hash = ?,\n is_without_password = ?,\n\n maybe_feature_flags = ?,\n\n ip_address_creation = ?,\n ip_address_last_login = ?,\n ip_address_last_update = ?,\n\n maybe_source = ?,\n\n maybe_signup_method = ?,\n\n maybe_referral_url = ?\n ",
|
||||
"query": "\nINSERT INTO users\nSET\n token = ?,\n \n is_temporary = ?,\n \n username = ?,\n display_name = ?,\n\n username_is_generated = ?,\n username_is_not_customized = ?,\n\n email_address = ?,\n email_gravatar_hash = ?,\n\n email_confirmed = FALSE,\n email_confirmed_by_google = ?,\n email_is_synthetic = ?,\n \n was_eagerly_provisioned = ?,\n\n profile_markdown = ?,\n profile_rendered_html = ?,\n user_role_slug = ?,\n\n password_hash = ?,\n is_without_password = ?,\n\n maybe_feature_flags = ?,\n\n ip_address_creation = ?,\n ip_address_last_login = ?,\n ip_address_last_update = ?,\n\n maybe_source = ?,\n\n maybe_signup_method = ?,\n\n maybe_referral_url = ?,\n maybe_landing_url = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 23
|
||||
"Right": 24
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "08996721482fe897c2721298c04cf0eba5fcf1d04d57397b8afab3dd59feaa8c"
|
||||
"hash": "3aacc0d270a9073ee5a38595cc32ab7793e0e8b9ce3ad81f554e7198fd0fd509"
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n id as `id: u64`,\n token as `token: tokens::tokens::characters::CharacterToken`,\n character_type as `character_type: enums::by_table::characters::character_type::CharacterType`,\n is_active as `is_active: bool`,\n character_name,\n maybe_description,\n maybe_avatar_media_token as `maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_full_image_media_token as `maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_original_upload_media_token as `maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n kinovi_character_id,\n kinovi_character_name,\n maybe_kinovi_asset_id\nFROM characters\nWHERE maybe_creator_user_token = ?\n AND is_active = true\n AND deleted_at IS NULL\n AND id < ?\nORDER BY id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "token: tokens::tokens::characters::CharacterToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "character_type: enums::by_table::characters::character_type::CharacterType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 96
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "is_active: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "character_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_description",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 2048
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "kinovi_character_id",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 256
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "kinovi_character_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "maybe_kinovi_asset_id",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 256
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "8c130ebd0d192355844863b3ebd44952aa8715eebe96375fb28f9f12baf7f9b1"
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"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 `kinovi_character_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`\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": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "kinovi_character_id",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 256
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "9bd472515a1ec55a4b3eed9b37baea3a9740484172086b1797889a5f88b5750f"
|
||||
}
|
||||
+3
-3
@@ -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\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 `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": [
|
||||
{
|
||||
@@ -85,7 +85,7 @@
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
@@ -98,5 +98,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cb33fd483f0fef7ec2a1f5950218f312a19e38c7971b88db949ccaa05bf7b7c4"
|
||||
"hash": "a0002183d4d077b07bb0cd062ca95bf113d1564ce3195f0e62e531ce4297dd76"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE characters\nSET\n is_active = true,\n maybe_avatar_media_token = ?,\n maybe_full_image_media_token = ?,\n maybe_kinovi_asset_id = ?\nWHERE token = ?\n AND deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "abbd24f51fae54ba3cd54ec7e3caf1ed7121b244796331d56b2d7f20eb7d4b9b"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = NULL,\n\n maybe_wallet_ledger_entry_token = NULL,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = NULL,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 12
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b0ba0fe63908df3d66b4820ca47cbb773786e480464c84a278fe63b1fa8913b4"
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n token as `token: tokens::tokens::characters::CharacterToken`,\n character_type as `character_type: enums::by_table::characters::character_type::CharacterType`,\n is_active as `is_active: bool`,\n character_name,\n maybe_description,\n maybe_avatar_media_token as `maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_full_image_media_token as `maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_original_upload_media_token as `maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n kinovi_character_id,\n kinovi_character_name,\n maybe_kinovi_asset_id\nFROM characters\nWHERE token = ?\n AND deleted_at IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token: tokens::tokens::characters::CharacterToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "character_type: enums::by_table::characters::character_type::CharacterType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 96
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "is_active: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "character_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_description",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 2048
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "kinovi_character_id",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 256
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "kinovi_character_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_kinovi_asset_id",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 256
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e9078c593087df3da20dbd193cdad102d6bb3ae83ee81c8c4ad75fe184e03c92"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE characters\nSET\n character_name = ?,\n kinovi_character_name = ?,\n maybe_description = ?\nWHERE token = ?\n AND deleted_at IS NULL\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f54e22efa17f3b8d36387dfd724e1fd0c88634fc8defa420ddd2de7e9103518c"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS characters;
|
||||
@@ -0,0 +1,101 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Characters are reusable entities that can be referenced in video/image generations.
|
||||
-- A character is created from one or more reference images and registered with third-party
|
||||
-- systems (e.g. Kinovi/Seedance) that support character-based generation.
|
||||
CREATE TABLE characters (
|
||||
-- Not used for anything except replication (private, not even used in queries)
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Public-facing "primary key" (PUBLIC)
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The type or "features" of the character.
|
||||
character_type VARCHAR(24) NOT NULL,
|
||||
|
||||
-- Whether the character has been fully created and is available for use.
|
||||
-- A character may exist in the database before its creation job completes.
|
||||
is_active BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- ========== CHARACTER DETAILS ==========
|
||||
|
||||
-- User's display name for the character.
|
||||
character_name VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- Optional user description of the character.
|
||||
maybe_description VARCHAR(512) DEFAULT NULL,
|
||||
|
||||
-- ========== MEDIA ==========
|
||||
|
||||
-- Avatar image (cropped to face, ideally).
|
||||
maybe_avatar_media_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Full-size reference image.
|
||||
maybe_full_image_media_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- TEMPORARY FOR GOING FAST :
|
||||
-- WE'LL NEED TO BREAK THIS INTO ANOTHER TABLE IF WE WANT TO SUPPORT MULTIPLE IMAGES.
|
||||
-- Image the character was originally created with.
|
||||
maybe_original_upload_media_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== CREATOR ==========
|
||||
|
||||
-- The user who created this character.
|
||||
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- For abuse tracking.
|
||||
creator_ip_address VARCHAR(40) NOT NULL,
|
||||
|
||||
-- ========== KINOVI (TEMPORARY) ==========
|
||||
|
||||
-- TEMPORARY FOR GOING FAST :
|
||||
-- IDEALLY WE MOVE THIS TO A SEPARATE TABLE.
|
||||
-- The identifier in the third-party system.
|
||||
-- e.g. Kinovi's "char_1774752056469_2wlxoq"
|
||||
kinovi_character_id VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- TEMPORARY FOR GOING FAST :
|
||||
-- IDEALLY WE MOVE THIS TO A SEPARATE TABLE.
|
||||
-- The name used by the third party.
|
||||
-- Important for Kinovi since characters are referenced by name in prompts.
|
||||
kinovi_character_name VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- TEMPORARY FOR GOING FAST :
|
||||
-- IDEALLY WE MOVE THIS TO A SEPARATE TABLE.
|
||||
-- This might be the upstream third party of kinovi.
|
||||
-- e.g. Kinovi's assetId "asset-20260329104101-kk5kl"
|
||||
maybe_kinovi_asset_id VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- TEMPORARY FOR GOING FAST :
|
||||
-- IDEALLY WE MOVE THIS TO A SEPARATE TABLE.
|
||||
-- Our job system token (if the character was created via a job).
|
||||
maybe_generic_inference_job_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Maybe this doesn't exist
|
||||
-- -- TEMPORARY FOR GOING FAST :
|
||||
-- -- IDEALLY WE MOVE THIS TO A SEPARATE TABLE.
|
||||
-- -- Third party's job system token (if any).
|
||||
-- maybe_kinovi_job_id VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- ========== TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP DEFAULT NULL,
|
||||
|
||||
-- ========== INDICES ==========
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (token),
|
||||
KEY index_character_type (character_type),
|
||||
KEY index_maybe_creator_user_token (maybe_creator_user_token),
|
||||
KEY index_maybe_creator_user_token_active (maybe_creator_user_token, is_active),
|
||||
KEY index_is_active (is_active),
|
||||
KEY index_kinovi_character_id (kinovi_character_id),
|
||||
KEY index_maybe_generic_inference_job_token (maybe_generic_inference_job_token),
|
||||
KEY index_created_at (created_at),
|
||||
KEY index_deleted_at (deleted_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE users
|
||||
DROP COLUMN maybe_landing_url;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN maybe_landing_url VARCHAR(255) DEFAULT NULL;
|
||||
@@ -15,6 +15,9 @@ CREATE TABLE characters (
|
||||
-- Effective "primary key" (PUBLIC)
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The type or "features" of the character.
|
||||
character_type VARCHAR(24) NOT NULL,
|
||||
|
||||
-- Whether the character has been fully created and is available for use.
|
||||
-- A character may exist in the database before its creation job completes.
|
||||
is_active BOOLEAN NOT NULL DEFAULT false,
|
||||
@@ -22,7 +25,7 @@ CREATE TABLE characters (
|
||||
-- ========== CHARACTER DETAILS ==========
|
||||
|
||||
-- Display name for the character.
|
||||
name VARCHAR(255) NOT NULL,
|
||||
character_name VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- Optional description of the character.
|
||||
maybe_description VARCHAR(512) DEFAULT NULL,
|
||||
@@ -35,6 +38,11 @@ CREATE TABLE characters (
|
||||
-- Full-size reference image.
|
||||
maybe_full_image_media_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- TEMPORARY FOR GOING FAST :
|
||||
-- WE'LL NEED TO BREAK THIS INTO ANOTHER TABLE IF WE WANT TO SUPPORT MULTIPLE IMAGES.
|
||||
-- Image the character was originally created with.
|
||||
maybe_original_upload_media_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== CREATOR ==========
|
||||
|
||||
-- The user who created this character.
|
||||
@@ -43,6 +51,37 @@ CREATE TABLE characters (
|
||||
-- For abuse tracking.
|
||||
creator_ip_address VARCHAR(40) NOT NULL,
|
||||
|
||||
-- ========== KINOVI (TEMPORARY) ==========
|
||||
|
||||
-- TEMPORARY FOR GOING FAST :
|
||||
-- IDEALLY WE MOVE THIS TO A SEPARATE TABLE.
|
||||
-- The identifier in the third-party system.
|
||||
-- e.g. Kinovi's "char_1774752056469_2wlxoq"
|
||||
kinovi_character_id VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- TEMPORARY FOR GOING FAST :
|
||||
-- IDEALLY WE MOVE THIS TO A SEPARATE TABLE.
|
||||
-- The name used by the third party.
|
||||
-- Important for Kinovi since characters are referenced by name in prompts.
|
||||
kinovi_character_name VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- TEMPORARY FOR GOING FAST :
|
||||
-- IDEALLY WE MOVE THIS TO A SEPARATE TABLE.
|
||||
-- This might be the upstream third party of kinovi.
|
||||
-- e.g. Kinovi's assetId "asset-20260329104101-kk5kl"
|
||||
maybe_kinovi_asset_id VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- TEMPORARY FOR GOING FAST :
|
||||
-- IDEALLY WE MOVE THIS TO A SEPARATE TABLE.
|
||||
-- Our job system token (if the character was created via a job).
|
||||
maybe_generic_inference_job_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Maybe this doesn't exist
|
||||
-- -- TEMPORARY FOR GOING FAST :
|
||||
-- -- IDEALLY WE MOVE THIS TO A SEPARATE TABLE.
|
||||
-- -- Third party's job system token (if any).
|
||||
-- maybe_kinovi_job_id VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- ========== TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -53,8 +92,13 @@ CREATE TABLE characters (
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (token),
|
||||
KEY index_character_type (character_type),
|
||||
KEY index_maybe_creator_user_token (maybe_creator_user_token),
|
||||
KEY index_maybe_creator_user_token_active (maybe_creator_user_token, is_active),
|
||||
KEY index_is_active (is_active),
|
||||
KEY index_created_at (created_at)
|
||||
KEY index_kinovi_character_id (kinovi_character_id),
|
||||
KEY index_maybe_generic_inference_job_token (maybe_generic_inference_job_token),
|
||||
KEY index_created_at (created_at),
|
||||
KEY index_deleted_at (deleted_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
@@ -222,8 +222,13 @@ CREATE TABLE users (
|
||||
was_eagerly_provisioned BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- The referral URL the user arrived from when signing up.
|
||||
-- This helps us know what marketing is effective.
|
||||
maybe_referral_url VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- The actual URL the user first arrived at when signing up, including query params.
|
||||
-- This helps us know what marketing is effective.
|
||||
maybe_landing_url VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- ========== MODERATION DETAILS ==========
|
||||
|
||||
-- Different than deleted.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
$kind: collection
|
||||
name: characters
|
||||
order: 4767
|
||||
@@ -0,0 +1,16 @@
|
||||
$kind: http-request
|
||||
name: Create Character
|
||||
url: "{{api_host}}/v1/character/create"
|
||||
method: POST
|
||||
body:
|
||||
type: json
|
||||
content: >-
|
||||
{
|
||||
"uuid_idempotency_token": "{{$randomUUID}}",
|
||||
"model": "seedance_2p0",
|
||||
//"image_media_token": "m_v2hjh936k72knmggmqqa2xfs9sge8k", // steampunk clown
|
||||
//"image_media_token": "m_13m13fm33bw3cxvfrncyc6873z0dyy", // mochi on couch
|
||||
"image_media_token": "m_cp20gcxsattt6j7699brs4ndqf803z", // t-rex
|
||||
"character_name": "T-rex"
|
||||
}
|
||||
order: 1775175681198
|
||||
@@ -0,0 +1,5 @@
|
||||
$kind: http-request
|
||||
name: Delete Character
|
||||
url: "{{api_host}}/v1/character/character_c40dzeq9pa1qtfy19q9f1w"
|
||||
method: DELETE
|
||||
order: 76446840865701
|
||||
@@ -0,0 +1,18 @@
|
||||
$kind: http-request
|
||||
name: Edit Character
|
||||
url: "{{api_host}}/v1/character/edit"
|
||||
method: POST
|
||||
body:
|
||||
type: json
|
||||
content: |-
|
||||
{
|
||||
/// The character to edit.
|
||||
"token": "character_4aj66rcqsbjmv3jp5ek4fc",
|
||||
|
||||
/// Updated name (if changing).
|
||||
"updated_name": "Mochi",
|
||||
|
||||
/// Updated description (if changing).
|
||||
"updated_description": "New Description"
|
||||
}
|
||||
order: 487436956121943
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
$kind: http-request
|
||||
name: Seedance 2.0 (character ref to video)
|
||||
url: "{{api_host}}/v1/generate/video/multi_function/seedance_2p0"
|
||||
method: POST
|
||||
headers:
|
||||
- key: Accept
|
||||
value: application/json
|
||||
- key: Session
|
||||
value: ""
|
||||
disabled: true
|
||||
- key: Session
|
||||
value: ""
|
||||
disabled: true
|
||||
body:
|
||||
type: json
|
||||
content: >
|
||||
{
|
||||
"uuid_idempotency_token": "{{$randomUUID}}",
|
||||
"prompt": "@Mochi is riding on the shoulder of @Steampunk Clown while they're walking through a crowded shopping mall in the sci-fi future. Retro 80's aesthetics while simultaneously futuristic and steam punk. @Mochi licks her lips and barks.",
|
||||
"duration_seconds": 5,
|
||||
"reference_character_tokens": ["character_beph3xd1z2m4kkbedra24f", "character_4aj66rcqsbjmv3jp5ek4fc"]
|
||||
}
|
||||
order: 3802
|
||||
@@ -8,6 +8,6 @@ body:
|
||||
type: json
|
||||
content: |-
|
||||
{
|
||||
"google_credential": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImIyN...[redacted]"
|
||||
"google_credential": "...[redacted]"
|
||||
}
|
||||
order: 4000
|
||||
|
||||
@@ -16,6 +16,12 @@ pub struct CreateCharacterRequest {
|
||||
|
||||
/// The reference image media file token.
|
||||
pub image_media_token: MediaFileToken,
|
||||
|
||||
/// Name of the character.
|
||||
pub character_name: String,
|
||||
|
||||
/// Description of the character.
|
||||
pub character_description: Option<String>,
|
||||
}
|
||||
|
||||
/// Response body for creating a character.
|
||||
|
||||
+5
@@ -1,4 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
@@ -29,6 +30,10 @@ pub struct Seedance2p0MultiFunctionVideoGenRequest {
|
||||
/// Optional reference audio files.
|
||||
pub reference_audio_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Optional character tokens to reference in the prompt.
|
||||
/// Characters are referenced in prompts as @CharacterName.
|
||||
pub reference_character_tokens: Option<Vec<CharacterToken>>,
|
||||
|
||||
/// Video aspect ratio / resolution.
|
||||
pub aspect_ratio: Option<Seedance2p0AspectRatio>,
|
||||
|
||||
|
||||
+5
-1
@@ -12,10 +12,14 @@ pub struct StripeArtcraftCreateSubscriptionCheckoutWithUserSignupRequest {
|
||||
|
||||
pub cadence: Option<PlanBillingCadence>,
|
||||
|
||||
/// Optional: The referral URL the user arrived from when signing up.
|
||||
/// Optional: The referral URL the user arrived from when first hitting the site, prior to navigation and signing up.
|
||||
/// The browser can send `document.referrer` to the backend so we know how people are finding us.
|
||||
/// If the browser doesn't send this parameter, we'll try the `referer` header.
|
||||
pub maybe_referral_url: Option<String>,
|
||||
|
||||
/// Optional: The URL where the user landed when they first arrived, prior to navigation and signing up.
|
||||
/// The browser can send `window.location.href` to the backend so we know how people are finding us.
|
||||
pub maybe_landing_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
/// Forward-compatible character reference list.
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum CharacterListRef<'a> {
|
||||
CharacterTokens(&'a Vec<CharacterToken>),
|
||||
|
||||
// In the future, we may have other identifiers for characters.
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod audio_list_ref;
|
||||
pub mod character_list_ref;
|
||||
pub mod common_aspect_ratio;
|
||||
pub mod common_image_model;
|
||||
pub mod common_resolution;
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ mod tests {
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ mod tests {
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
+1
@@ -74,6 +74,7 @@ mod tests {
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
+2
@@ -34,6 +34,7 @@ pub (crate) fn estimate_video_cost_artcraft_seedance2p0(
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
@@ -68,6 +69,7 @@ mod tests {
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ pub(crate) fn estimate_video_cost_seedance2pro_seedance2p0(
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ pub async fn execute_artcraft_seedance2p0(
|
||||
reference_image_media_tokens: plan.reference_images.map(|tokens| tokens.to_owned()),
|
||||
reference_video_media_tokens: plan.reference_videos.map(|tokens| tokens.to_owned()),
|
||||
reference_audio_media_tokens: plan.reference_audio.map(|tokens| tokens.to_owned()),
|
||||
reference_character_tokens: plan.reference_characters.map(|tokens| tokens.to_owned()),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
duration_seconds: plan.duration_seconds,
|
||||
batch_count: Some(plan.batch_count),
|
||||
|
||||
+1
@@ -44,6 +44,7 @@ pub async fn execute_seedance2pro_seedance2p0(
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
|
||||
+5
@@ -1,7 +1,9 @@
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
@@ -47,6 +49,9 @@ pub struct GenerateVideoRequest<'a> {
|
||||
/// Reference audio (optional).
|
||||
pub reference_audio: Option<AudioListRef<'a>>,
|
||||
|
||||
/// Reference characters (optional).
|
||||
pub reference_character_tokens: Option<CharacterListRef<'a>>,
|
||||
|
||||
/// The resolution to use
|
||||
pub resolution: Option<CommonResolution>,
|
||||
|
||||
|
||||
+1
@@ -180,6 +180,7 @@ mod tests {
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
|
||||
+1
@@ -180,6 +180,7 @@ mod tests {
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
|
||||
+1
@@ -197,6 +197,7 @@ mod tests {
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
|
||||
+12
@@ -11,6 +11,7 @@ use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
|
||||
Seedance2p0AspectRatio, Seedance2p0BatchCount,
|
||||
};
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -21,6 +22,7 @@ pub struct PlanArtcraftSeedance2p0<'a> {
|
||||
pub reference_images: Option<&'a Vec<MediaFileToken>>,
|
||||
pub reference_videos: Option<&'a Vec<MediaFileToken>>,
|
||||
pub reference_audio: Option<&'a Vec<MediaFileToken>>,
|
||||
pub reference_characters: Option<&'a Vec<CharacterToken>>,
|
||||
pub aspect_ratio: Option<Seedance2p0AspectRatio>,
|
||||
pub duration_seconds: Option<u8>,
|
||||
pub batch_count: Seedance2p0BatchCount,
|
||||
@@ -49,6 +51,7 @@ pub fn plan_generate_video_artcraft_seedance2p0<'a>(
|
||||
reference_images,
|
||||
reference_videos,
|
||||
reference_audio,
|
||||
reference_characters: resolve_character_list_ref(request.reference_character_tokens),
|
||||
aspect_ratio,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
@@ -56,6 +59,15 @@ pub fn plan_generate_video_artcraft_seedance2p0<'a>(
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_character_list_ref<'a>(
|
||||
character_list_ref: Option<crate::api::character_list_ref::CharacterListRef<'a>>,
|
||||
) -> Option<&'a Vec<CharacterToken>> {
|
||||
match character_list_ref {
|
||||
None => None,
|
||||
Some(crate::api::character_list_ref::CharacterListRef::CharacterTokens(tokens)) => Some(tokens),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_ref<'a>(
|
||||
image_ref: Option<ImageRef<'a>>,
|
||||
) -> Result<Option<&'a MediaFileToken>, ArtcraftRouterError> {
|
||||
|
||||
@@ -292,6 +292,7 @@ pub fn base_video_request() -> GenerateVideoRequest<'static> {
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
|
||||
@@ -49,6 +49,10 @@ pub struct GenerateVideoArgs<'a> {
|
||||
/// Sent in a separate `audioUrls` field (not in `uploadedUrls`).
|
||||
pub reference_audio_urls: Option<Vec<String>>,
|
||||
|
||||
/// Optional Kinovi character IDs to reference in the prompt.
|
||||
/// Characters are referenced in prompts as @CharacterName.
|
||||
pub character_ids: Option<Vec<String>>,
|
||||
|
||||
/// Controls the `faceBlurMode` field: true sends "on", false sends "off", None omits it.
|
||||
pub use_face_blur_hack: Option<bool>,
|
||||
|
||||
@@ -69,6 +73,7 @@ impl std::fmt::Debug for GenerateVideoArgs<'_> {
|
||||
.field("reference_image_urls", &self.reference_image_urls)
|
||||
.field("reference_video_urls", &self.reference_video_urls)
|
||||
.field("reference_audio_urls", &self.reference_audio_urls)
|
||||
.field("character_ids", &self.character_ids)
|
||||
.field("use_face_blur_hack", &self.use_face_blur_hack)
|
||||
.field("host_override", &self.host_override)
|
||||
.finish()
|
||||
@@ -261,6 +266,7 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
|
||||
duration,
|
||||
mode: video_input_mode,
|
||||
face_blur_mode,
|
||||
character_ids: args.character_ids,
|
||||
uploaded_urls,
|
||||
audio_urls,
|
||||
batch_count,
|
||||
@@ -371,6 +377,7 @@ mod tests {
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
}
|
||||
@@ -495,6 +502,7 @@ mod tests {
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
@@ -524,6 +532,7 @@ mod tests {
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
@@ -555,6 +564,7 @@ mod tests {
|
||||
]),
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
@@ -585,6 +595,7 @@ mod tests {
|
||||
"https://static.seedance2-pro.com/materials/20260315/1773594284659-3a46d231.mp4".to_string(),
|
||||
]),
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
@@ -617,6 +628,7 @@ mod tests {
|
||||
"https://static.seedance2-pro.com/materials/20260315/1773594284659-3a46d231.mp4".to_string(),
|
||||
]),
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
@@ -671,6 +683,7 @@ mod tests {
|
||||
result.public_url,
|
||||
]),
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
@@ -720,6 +733,7 @@ mod tests {
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
@@ -771,6 +785,7 @@ mod tests {
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
@@ -829,6 +844,7 @@ mod tests {
|
||||
reference_image_urls: Some(uploaded_urls),
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
@@ -880,6 +896,7 @@ mod tests {
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: Some(vec![upload_result.public_url]),
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
@@ -891,5 +908,137 @@ mod tests {
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod character_tests {
|
||||
use super::*;
|
||||
|
||||
const STEAMPUNK_CLOWN_ID: &str = "char_1775176566518_sik0te";
|
||||
const MOCHI_ID: &str = "char_1775177718294_g2pitx";
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies, costs money
|
||||
async fn test_text_prompt_with_character_pro() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
let args = GenerateVideoArgs {
|
||||
session: &session,
|
||||
model_type: ModelType::Seedance2Pro,
|
||||
prompt: "@Steampunk Clown is juggling flaming torches in a circus tent.".to_string(),
|
||||
resolution: Resolution::Landscape16x9,
|
||||
duration_seconds: 5,
|
||||
batch_count: BatchCount::One,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: Some(vec![STEAMPUNK_CLOWN_ID.to_string()]),
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
let result = generate_video(args).await?;
|
||||
println!("Task ID: {}", result.task_id);
|
||||
println!("Order ID: {}", result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert!(!result.order_id.is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies, costs money
|
||||
async fn test_text_prompt_with_character_fast() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
let args = GenerateVideoArgs {
|
||||
session: &session,
|
||||
model_type: ModelType::Seedance2Fast,
|
||||
prompt: "@Mochi the female shiba inu is eating a cheese pizza while standing on the table".to_string(),
|
||||
resolution: Resolution::Portrait9x16,
|
||||
duration_seconds: 5,
|
||||
batch_count: BatchCount::One,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: Some(vec![MOCHI_ID.to_string()]),
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
let result = generate_video(args).await?;
|
||||
println!("Task ID: {}", result.task_id);
|
||||
println!("Order ID: {}", result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert!(!result.order_id.is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies, costs money
|
||||
async fn test_character_with_image_ref_pro() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
let args = GenerateVideoArgs {
|
||||
session: &session,
|
||||
model_type: ModelType::Seedance2Pro,
|
||||
prompt: "@Steampunk Clown is walking up to pet a dog on the couch.".to_string(),
|
||||
resolution: Resolution::Landscape16x9,
|
||||
duration_seconds: 5,
|
||||
batch_count: BatchCount::One,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: Some(vec![
|
||||
"https://static.seedance2-pro.com/materials/20260329/1774752385699-1ff44886.jpeg".to_string(),
|
||||
]),
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: Some(vec![STEAMPUNK_CLOWN_ID.to_string()]),
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
let result = generate_video(args).await?;
|
||||
println!("Task ID: {}", result.task_id);
|
||||
println!("Order ID: {}", result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert!(!result.order_id.is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies, costs money
|
||||
async fn test_two_characters_fast() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
let args = GenerateVideoArgs {
|
||||
session: &session,
|
||||
model_type: ModelType::Seedance2Fast,
|
||||
prompt: "@Steampunk Clown and @Mochi are playing fetch in a sunny park.".to_string(),
|
||||
resolution: Resolution::Landscape16x9,
|
||||
duration_seconds: 5,
|
||||
batch_count: BatchCount::One,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: Some(vec![
|
||||
STEAMPUNK_CLOWN_ID.to_string(),
|
||||
MOCHI_ID.to_string(),
|
||||
]),
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
let result = generate_video(args).await?;
|
||||
println!("Task ID: {}", result.task_id);
|
||||
println!("Order ID: {}", result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert!(!result.order_id.is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ pub (super) struct ApiParams {
|
||||
pub mode: &'static str,
|
||||
#[serde(rename = "faceBlurMode", skip_serializing_if = "Option::is_none")]
|
||||
pub face_blur_mode: Option<&'static str>,
|
||||
#[serde(rename = "characterIds", skip_serializing_if = "Option::is_none")]
|
||||
pub character_ids: Option<Vec<String>>,
|
||||
#[serde(rename = "uploadedUrls", skip_serializing_if = "Option::is_none")]
|
||||
pub uploaded_urls: Option<Vec<String>>,
|
||||
#[serde(rename = "audioUrls", skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -4,4 +4,5 @@ pub mod kinovi_host;
|
||||
pub mod poll_characters;
|
||||
pub mod poll_orders;
|
||||
pub mod prepare_file_upload;
|
||||
pub mod update_character;
|
||||
pub mod upload_file;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod update_character;
|
||||
pub(crate) mod request_types;
|
||||
@@ -0,0 +1,44 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
// ======================== Request ========================
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub(super) struct BatchRequest {
|
||||
#[serde(rename = "0")]
|
||||
pub zero: BatchRequestInner,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub(super) struct BatchRequestInner {
|
||||
pub json: BatchRequestJson,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct BatchRequestJson {
|
||||
pub character_id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
// ======================== Response ========================
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct BatchResponseItem {
|
||||
pub result: BatchResponseResult,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct BatchResponseResult {
|
||||
pub data: BatchResponseData,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct BatchResponseData {
|
||||
pub json: UpdateCharacterResponseJson,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct UpdateCharacterResponseJson {
|
||||
pub success: bool,
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::requests::update_character::request_types::*;
|
||||
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
|
||||
use crate::utils::common_headers::FIREFOX_USER_AGENT;
|
||||
use log::info;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
|
||||
// --- Public args ---
|
||||
|
||||
pub struct UpdateCharacterArgs<'a> {
|
||||
pub session: &'a Seedance2ProSession,
|
||||
|
||||
/// The Kinovi character ID (e.g. "char_1775184656440_vsy3eu").
|
||||
pub character_id: String,
|
||||
|
||||
/// The new name for the character.
|
||||
pub name: String,
|
||||
|
||||
/// The new description (empty string clears it).
|
||||
pub description: String,
|
||||
|
||||
/// Override the default host (kinovi.ai).
|
||||
pub host_override: Option<KinoviHost>,
|
||||
}
|
||||
|
||||
// --- Public response ---
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UpdateCharacterResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
// --- Implementation ---
|
||||
|
||||
pub async fn update_character(args: UpdateCharacterArgs<'_>) -> Result<UpdateCharacterResponse, Seedance2ProError> {
|
||||
let host = resolve_host(args.host_override.as_ref());
|
||||
let base_url = host.api_base_url();
|
||||
let url = format!("{}/api/trpc/character.updateCharacter?batch=1", base_url);
|
||||
|
||||
info!("Updating character '{}' (name='{}', description='{}')", args.character_id, args.name, args.description);
|
||||
|
||||
let request_body = BatchRequest {
|
||||
zero: BatchRequestInner {
|
||||
json: BatchRequestJson {
|
||||
character_id: args.character_id.clone(),
|
||||
name: args.name,
|
||||
description: args.description,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let cookie = args.session.cookies.as_str();
|
||||
let referer = format!("{}/app/characters", base_url);
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox143)
|
||||
.build()
|
||||
.map_err(|err| Seedance2ProClientError::WreqClientError(err))?;
|
||||
|
||||
let response = client.post(&url)
|
||||
.header("User-Agent", FIREFOX_USER_AGENT)
|
||||
.header("Accept", "*/*")
|
||||
.header("Accept-Language", "en-US,en;q=0.9")
|
||||
.header("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
.header("Referer", &referer)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("x-trpc-source", "client")
|
||||
.header("Origin", base_url)
|
||||
.header("Connection", "keep-alive")
|
||||
.header("Cookie", cookie)
|
||||
.header("Sec-Fetch-Dest", "empty")
|
||||
.header("Sec-Fetch-Mode", "cors")
|
||||
.header("Sec-Fetch-Site", "same-origin")
|
||||
.header("Priority", "u=4")
|
||||
.header("TE", "trailers")
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| Seedance2ProGenericApiError::WreqError(err))?;
|
||||
|
||||
let status = response.status();
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| Seedance2ProGenericApiError::WreqError(err))?;
|
||||
|
||||
info!("Update character response: status={}, body={}", status, response_body);
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: status,
|
||||
body: response_body,
|
||||
}.into());
|
||||
}
|
||||
|
||||
let batch_response: Vec<BatchResponseItem> = serde_json::from_str(&response_body)
|
||||
.map_err(|err| Seedance2ProGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.clone()))?;
|
||||
|
||||
let data = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
Ok(UpdateCharacterResponse {
|
||||
success: data.success,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::test_utils::get_test_cookies::get_test_cookies;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
|
||||
fn test_session() -> AnyhowResult<Seedance2ProSession> {
|
||||
let cookies = get_test_cookies()?;
|
||||
Ok(Seedance2ProSession::from_cookies_string(cookies))
|
||||
}
|
||||
|
||||
// Steampunk Clown character
|
||||
const TEST_CHARACTER_ID: &str = "char_1775176566518_sik0te";
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies, modifies real data
|
||||
async fn test_rename_character() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
|
||||
let result = update_character(UpdateCharacterArgs {
|
||||
session: &session,
|
||||
character_id: TEST_CHARACTER_ID.to_string(),
|
||||
name: "Steampunk Clown (renamed)".to_string(),
|
||||
description: "A steampunk clown character for testing".to_string(),
|
||||
host_override: None,
|
||||
}).await?;
|
||||
|
||||
println!("Success: {}", result.success);
|
||||
assert!(result.success);
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies, modifies real data
|
||||
async fn test_rename_character_clear_description() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
|
||||
let result = update_character(UpdateCharacterArgs {
|
||||
session: &session,
|
||||
character_id: TEST_CHARACTER_ID.to_string(),
|
||||
name: "Steampunk Clown".to_string(),
|
||||
description: "".to_string(),
|
||||
host_override: None,
|
||||
}).await?;
|
||||
|
||||
println!("Success: {}", result.success);
|
||||
assert!(result.success);
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies, modifies real data
|
||||
async fn test_rename_and_restore() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
|
||||
// Rename
|
||||
let result = update_character(UpdateCharacterArgs {
|
||||
session: &session,
|
||||
character_id: TEST_CHARACTER_ID.to_string(),
|
||||
name: "Temporary Name".to_string(),
|
||||
description: "Temporary description".to_string(),
|
||||
host_override: None,
|
||||
}).await?;
|
||||
assert!(result.success);
|
||||
|
||||
// Restore
|
||||
let result = update_character(UpdateCharacterArgs {
|
||||
session: &session,
|
||||
character_id: TEST_CHARACTER_ID.to_string(),
|
||||
name: "Steampunk Clown".to_string(),
|
||||
description: "A steampunk clown".to_string(),
|
||||
host_override: None,
|
||||
}).await?;
|
||||
assert!(result.success);
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ async fn seed_user(
|
||||
ip_address: "127.0.0.1",
|
||||
maybe_source: None,
|
||||
maybe_referral_url: None,
|
||||
maybe_landing_url: None,
|
||||
maybe_user_token: maybe_user_token.as_ref(),
|
||||
}).await.map_err(|err| anyhow!("err: {:?}", err))?;
|
||||
|
||||
|
||||
+5
-3
@@ -3,12 +3,14 @@ use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_comma
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel;
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_router::api::audio_list_ref::AudioListRef;
|
||||
use artcraft_router::api::character_list_ref::CharacterListRef;
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
use artcraft_router::api::video_list_ref::VideoListRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::api::video_list_ref::VideoListRef;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use artcraft_router::client::router_client::RouterClient;
|
||||
@@ -16,7 +18,6 @@ use artcraft_router::generate::generate_video::generate_video_request::GenerateV
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use log::{error, info};
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
|
||||
pub(super) async fn handle_artcraft_seedance_2p0(
|
||||
request: &EnqueueImageToVideoRequest,
|
||||
@@ -42,6 +43,7 @@ pub(super) async fn handle_artcraft_seedance_2p0(
|
||||
reference_images,
|
||||
reference_videos: request.reference_video_media_tokens.as_ref().map(VideoListRef::MediaFileTokens),
|
||||
reference_audio: request.reference_audio_media_tokens.as_ref().map(AudioListRef::MediaFileTokens),
|
||||
reference_character_tokens: request.reference_character_tokens.as_ref().map(CharacterListRef::CharacterTokens),
|
||||
resolution: None,
|
||||
aspect_ratio: request.aspect_ratio,
|
||||
duration_seconds: request.duration_seconds,
|
||||
|
||||
+2
@@ -3,6 +3,7 @@ use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_comma
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use artcraft_router::api::character_list_ref::CharacterListRef;
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel;
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
@@ -40,6 +41,7 @@ pub(super) async fn handle_artcraft_video_via_router(
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: request.reference_character_tokens.as_ref().map(CharacterListRef::CharacterTokens),
|
||||
resolution: None,
|
||||
aspect_ratio: request.aspect_ratio,
|
||||
duration_seconds: request.duration_seconds,
|
||||
|
||||
+5
@@ -29,6 +29,7 @@ use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use log::{error, info, warn};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, State};
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// This is used in the Tauri command bridge.
|
||||
@@ -123,6 +124,10 @@ pub struct EnqueueImageToVideoRequest {
|
||||
/// Reference audio for the video generation (e.g. voices, lip sync, music, etc.)
|
||||
pub reference_audio_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Optional character tokens to reference in the prompt.
|
||||
/// Characters are referenced in prompts as @CharacterName.
|
||||
pub reference_character_tokens: Option<Vec<CharacterToken>>,
|
||||
|
||||
/// Optional.
|
||||
/// Text prompt used to direct the video.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
/// The version of the ArtCraft desktop app reported in the UI and via Analytics.
|
||||
/// This should match the CI builds.
|
||||
pub const ARTCRAFT_VERSION: &str = "0.20.0";
|
||||
pub const ARTCRAFT_VERSION: &str = "0.22.0";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.20.0",
|
||||
"version": "0.22.0",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/artcraft/dist",
|
||||
@@ -14,6 +14,10 @@
|
||||
"windows": [
|
||||
],
|
||||
"security": {
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": ["**"]
|
||||
},
|
||||
"csp": {
|
||||
"default-src": [
|
||||
"'self'",
|
||||
@@ -32,6 +36,7 @@
|
||||
"'self'",
|
||||
"'unsafe-eval'",
|
||||
"'unsafe-inline'",
|
||||
"asset:",
|
||||
"blob:",
|
||||
"data:",
|
||||
"http://ipc.localhost",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.20.0",
|
||||
"version": "0.22.0",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/artcraft/dist",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::notification::generate_deduplication_key::generate_deduplication_key;
|
||||
use crate::notification::notification_details_builder::NotificationDetailsBuilder;
|
||||
use crate::notification::notification_urgency::NotificationUrgency;
|
||||
|
||||
/// Details for a pager notification.
|
||||
@@ -20,7 +19,7 @@ pub struct NotificationDetails {
|
||||
pub(crate) event_time: DateTime<Utc>,
|
||||
|
||||
/// The error that triggered this notification, if any.
|
||||
pub(crate) maybe_error: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
|
||||
pub(crate) maybe_error: Option<std::sync::Arc<dyn std::error::Error + Send + Sync + 'static>>,
|
||||
|
||||
/// Whether this notification originated from an error.
|
||||
pub(crate) is_from_error: bool,
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use crate::notification::notification_details::NotificationDetails;
|
||||
use crate::notification::notification_urgency::NotificationUrgency;
|
||||
|
||||
pub struct NotificationDetailsBuilder {
|
||||
title: String,
|
||||
pub(crate) title: Option<String>,
|
||||
pub(crate) description: Option<String>,
|
||||
pub(crate) urgency: Option<NotificationUrgency>,
|
||||
pub(crate) event_time: DateTime<Utc>,
|
||||
|
||||
pub(crate) maybe_error: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
|
||||
pub(crate) maybe_error: Option<Arc<dyn std::error::Error + Send + Sync + 'static>>,
|
||||
pub(crate) is_from_error: bool,
|
||||
|
||||
pub(crate) http_method: Option<String>,
|
||||
@@ -24,10 +25,12 @@ pub struct NotificationDetailsBuilder {
|
||||
}
|
||||
|
||||
impl NotificationDetailsBuilder {
|
||||
/// Create a builder from a summary string.
|
||||
// --- Constructors ---
|
||||
|
||||
/// Create a builder with an explicit title.
|
||||
pub fn from_title(title: String) -> Self {
|
||||
Self {
|
||||
title,
|
||||
title: Some(title),
|
||||
description: None,
|
||||
urgency: None,
|
||||
event_time: Utc::now(),
|
||||
@@ -43,13 +46,16 @@ impl NotificationDetailsBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a builder from a boxed error.
|
||||
///
|
||||
/// Sets the title to a placeholder and attaches the error object.
|
||||
/// Callers should chain `.set_title()` to provide a meaningful title.
|
||||
pub fn from_error(error: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
|
||||
/// Create a builder from a boxed error. Converts to `Arc` internally.
|
||||
pub fn from_boxed_error(error: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
|
||||
Self::from_error(Arc::from(error))
|
||||
}
|
||||
|
||||
/// Create a builder from an `Arc`'d error.
|
||||
/// Title will be auto-generated from the error if not explicitly set.
|
||||
pub fn from_error(error: Arc<dyn std::error::Error + Send + Sync + 'static>) -> Self {
|
||||
Self {
|
||||
title: "Notification from Error".to_string(),
|
||||
title: None,
|
||||
description: None,
|
||||
urgency: None,
|
||||
event_time: Utc::now(),
|
||||
@@ -65,8 +71,10 @@ impl NotificationDetailsBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Setters ---
|
||||
|
||||
pub fn set_title(mut self, title: String) -> Self {
|
||||
self.title = title;
|
||||
self.title = Some(title);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -80,7 +88,7 @@ impl NotificationDetailsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_error(mut self, error: Option<Box<dyn std::error::Error + Send + Sync + 'static>>) -> Self {
|
||||
pub fn set_error(mut self, error: Option<Arc<dyn std::error::Error + Send + Sync + 'static>>) -> Self {
|
||||
self.maybe_error = error;
|
||||
self
|
||||
}
|
||||
@@ -120,9 +128,16 @@ impl NotificationDetailsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
// --- Build ---
|
||||
|
||||
pub fn build(self) -> NotificationDetails {
|
||||
let title = match self.title {
|
||||
Some(t) => t,
|
||||
None => Self::generate_title(&self.maybe_error, &self.http_method, &self.http_path),
|
||||
};
|
||||
|
||||
NotificationDetails {
|
||||
title: self.title,
|
||||
title,
|
||||
description: self.description,
|
||||
urgency: self.urgency,
|
||||
event_time: self.event_time,
|
||||
@@ -137,4 +152,41 @@ impl NotificationDetailsBuilder {
|
||||
third_party_id: self.third_party_id,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Private helpers ---
|
||||
|
||||
/// Generate a reasonable title from available context.
|
||||
///
|
||||
/// Priority:
|
||||
/// 1. Error + HTTP info: "POST /v1/foo - SomeError: details"
|
||||
/// 2. Error alone: "SomeError: details"
|
||||
/// 3. HTTP info alone: "POST /v1/foo - Unknown Error"
|
||||
/// 4. Nothing: "Unknown Error"
|
||||
fn generate_title(
|
||||
maybe_error: &Option<Arc<dyn std::error::Error + Send + Sync + 'static>>,
|
||||
http_method: &Option<String>,
|
||||
http_path: &Option<String>,
|
||||
) -> String {
|
||||
let http_prefix = match (http_method, http_path) {
|
||||
(Some(method), Some(path)) => Some(format!("{} {}", method, path)),
|
||||
(None, Some(path)) => Some(path.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let error_summary = maybe_error.as_ref().map(|err| {
|
||||
let msg = format!("{}", err);
|
||||
if msg.len() > 150 {
|
||||
format!("{}...", &msg[..147])
|
||||
} else {
|
||||
msg
|
||||
}
|
||||
});
|
||||
|
||||
match (http_prefix, error_summary) {
|
||||
(Some(prefix), Some(summary)) => format!("{} - {}", prefix, summary),
|
||||
(None, Some(summary)) => summary,
|
||||
(Some(prefix), None) => format!("{} - Unknown Error", prefix),
|
||||
(None, None) => "Unknown Error".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,18 @@ pub enum DatabaseQueryError {
|
||||
AnyhowError(anyhow::Error),
|
||||
}
|
||||
|
||||
impl std::error::Error for DatabaseQueryError {}
|
||||
|
||||
impl std::fmt::Display for DatabaseQueryError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DatabaseQueryError::IdempotencyDuplicateKeyError => write!(f, "Idempotency duplicate key error"),
|
||||
DatabaseQueryError::SqlxError(err) => write!(f, "SQLX error: {}", err),
|
||||
DatabaseQueryError::AnyhowError(err) => write!(f, "Anyhow error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for DatabaseQueryError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
DatabaseQueryError::AnyhowError(err)
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Activate a character after its creation job completes successfully.
|
||||
///
|
||||
/// Sets `is_active = true`, attaches media tokens, and records the kinovi asset ID.
|
||||
pub async fn activate_character_with_media(
|
||||
character_token: &CharacterToken,
|
||||
maybe_avatar_media_token: Option<&MediaFileToken>,
|
||||
maybe_full_image_media_token: Option<&MediaFileToken>,
|
||||
maybe_kinovi_asset_id: Option<&str>,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE characters
|
||||
SET
|
||||
is_active = true,
|
||||
maybe_avatar_media_token = ?,
|
||||
maybe_full_image_media_token = ?,
|
||||
maybe_kinovi_asset_id = ?
|
||||
WHERE token = ?
|
||||
AND deleted_at IS NULL
|
||||
LIMIT 1
|
||||
"#,
|
||||
maybe_avatar_media_token.map(|t| t.as_str()),
|
||||
maybe_full_image_media_token.map(|t| t.as_str()),
|
||||
maybe_kinovi_asset_id,
|
||||
character_token.as_str(),
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{FromRow, MySql, QueryBuilder};
|
||||
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
/// Character data needed for constructing prompts with character references.
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct CharacterPromptData {
|
||||
pub token: CharacterToken,
|
||||
pub is_active: bool,
|
||||
pub character_name: Option<String>,
|
||||
pub kinovi_character_id: Option<String>,
|
||||
pub kinovi_character_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Look up multiple characters by their tokens, returning the data needed for prompting.
|
||||
///
|
||||
/// Characters that are not found (or soft-deleted) are silently omitted from the results.
|
||||
pub async fn batch_lookup_characters_by_token_for_prompting(
|
||||
tokens: &[CharacterToken],
|
||||
connection: &mut PoolConnection<MySql>,
|
||||
) -> Result<Vec<CharacterPromptData>, sqlx::Error> {
|
||||
if tokens.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new(
|
||||
r#"
|
||||
SELECT
|
||||
token,
|
||||
is_active,
|
||||
character_name,
|
||||
kinovi_character_id,
|
||||
kinovi_character_name
|
||||
FROM characters
|
||||
WHERE deleted_at IS NULL
|
||||
AND token IN (
|
||||
"#,
|
||||
);
|
||||
|
||||
let mut separated = query_builder.separated(", ");
|
||||
for token in tokens {
|
||||
separated.push_bind(token.as_str().to_string());
|
||||
}
|
||||
|
||||
query_builder.push(")");
|
||||
|
||||
let results: Vec<CharacterPromptData> = query_builder
|
||||
.build_query_as::<CharacterPromptData>()
|
||||
.fetch_all(&mut **connection)
|
||||
.await?;
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
use log::info;
|
||||
use sqlx::{Executor, MySql};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use enums::by_table::characters::character_type::CharacterType;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// Arguments for inserting a new pending character record.
|
||||
pub struct CreatePendingCharacterArgs<'e, 'c, E>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
/// The type of character (e.g. KinoviSeedance).
|
||||
pub character_type: CharacterType,
|
||||
|
||||
/// User's display name for the character (truncated to 255 chars by caller).
|
||||
pub character_name: &'e str,
|
||||
|
||||
/// Optional user description (truncated to 512 chars by caller).
|
||||
pub maybe_description: Option<&'e str>,
|
||||
|
||||
/// The media file token of the original uploaded image.
|
||||
pub maybe_original_upload_media_token: Option<&'e MediaFileToken>,
|
||||
|
||||
/// The user who created this character.
|
||||
pub maybe_creator_user_token: Option<&'e UserToken>,
|
||||
|
||||
/// IP address for abuse tracking.
|
||||
pub creator_ip_address: &'e str,
|
||||
|
||||
/// The Kinovi character ID (e.g. "char_1774752056469_2wlxoq").
|
||||
pub kinovi_character_id: &'e str,
|
||||
|
||||
/// The name Kinovi assigned to the character.
|
||||
pub kinovi_character_name: &'e str,
|
||||
|
||||
/// Our inference job token for tracking the character creation.
|
||||
pub maybe_generic_inference_job_token: Option<&'e InferenceJobToken>,
|
||||
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
/// Insert a new pending character record. Returns the generated CharacterToken.
|
||||
///
|
||||
/// The character is created with `is_active = false`. It will be activated
|
||||
/// once the async creation job completes.
|
||||
pub async fn create_pending_character<'e, 'c: 'e, E>(
|
||||
args: CreatePendingCharacterArgs<'e, 'c, E>,
|
||||
) -> Result<CharacterToken, sqlx::Error>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
let token = CharacterToken::generate();
|
||||
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO characters
|
||||
SET
|
||||
token = ?,
|
||||
character_type = ?,
|
||||
is_active = false,
|
||||
character_name = ?,
|
||||
maybe_description = ?,
|
||||
maybe_avatar_media_token = NULL,
|
||||
maybe_full_image_media_token = NULL,
|
||||
maybe_original_upload_media_token = ?,
|
||||
maybe_creator_user_token = ?,
|
||||
creator_ip_address = ?,
|
||||
kinovi_character_id = ?,
|
||||
kinovi_character_name = ?,
|
||||
maybe_kinovi_asset_id = NULL,
|
||||
maybe_generic_inference_job_token = ?
|
||||
"#,
|
||||
token.as_str(),
|
||||
args.character_type.to_str(),
|
||||
args.character_name,
|
||||
args.maybe_description,
|
||||
args.maybe_original_upload_media_token.map(|t| t.as_str()),
|
||||
args.maybe_creator_user_token.map(|t| t.as_str()),
|
||||
args.creator_ip_address,
|
||||
args.kinovi_character_id,
|
||||
args.kinovi_character_name,
|
||||
args.maybe_generic_inference_job_token.map(|t| t.as_str()),
|
||||
);
|
||||
|
||||
let result = query.execute(args.mysql_executor)
|
||||
.await?;
|
||||
|
||||
info!("Created pending character {} (record ID {})", token, result.last_insert_id());
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::MySql;
|
||||
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
/// Soft-delete a character by setting deleted_at to NOW().
|
||||
///
|
||||
/// This is idempotent — calling it on an already-deleted character is a no-op.
|
||||
pub async fn delete_character(
|
||||
character_token: &CharacterToken,
|
||||
connection: &mut PoolConnection<MySql>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE characters
|
||||
SET deleted_at = NOW()
|
||||
WHERE token = ?
|
||||
AND deleted_at IS NULL
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(character_token.as_str())
|
||||
.execute(&mut **connection)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::MySql;
|
||||
|
||||
use enums::by_table::characters::character_type::CharacterType;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// Full character record for the GET endpoint.
|
||||
pub struct CharacterRecord {
|
||||
pub token: CharacterToken,
|
||||
pub character_type: CharacterType,
|
||||
pub is_active: bool,
|
||||
pub character_name: Option<String>,
|
||||
pub maybe_description: Option<String>,
|
||||
pub maybe_avatar_media_token: Option<MediaFileToken>,
|
||||
pub maybe_full_image_media_token: Option<MediaFileToken>,
|
||||
pub maybe_original_upload_media_token: Option<MediaFileToken>,
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub kinovi_character_id: Option<String>,
|
||||
pub kinovi_character_name: Option<String>,
|
||||
pub maybe_kinovi_asset_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Look up a character by token (excluding soft-deleted).
|
||||
pub async fn get_character_by_token(
|
||||
character_token: &CharacterToken,
|
||||
connection: &mut PoolConnection<MySql>,
|
||||
) -> Result<Option<CharacterRecord>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
CharacterRecord,
|
||||
r#"
|
||||
SELECT
|
||||
token as `token: tokens::tokens::characters::CharacterToken`,
|
||||
character_type as `character_type: enums::by_table::characters::character_type::CharacterType`,
|
||||
is_active as `is_active: bool`,
|
||||
character_name,
|
||||
maybe_description,
|
||||
maybe_avatar_media_token as `maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken`,
|
||||
maybe_full_image_media_token as `maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken`,
|
||||
maybe_original_upload_media_token as `maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken`,
|
||||
maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
|
||||
kinovi_character_id,
|
||||
kinovi_character_name,
|
||||
maybe_kinovi_asset_id
|
||||
FROM characters
|
||||
WHERE token = ?
|
||||
AND deleted_at IS NULL
|
||||
"#,
|
||||
character_token.as_str(),
|
||||
)
|
||||
.fetch_optional(&mut **connection)
|
||||
.await
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{FromRow, MySql};
|
||||
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// Minimal character record for ownership and deletion checks.
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct CharacterOwnershipRecord {
|
||||
pub token: CharacterToken,
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub is_active: bool,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
/// Look up a character by token, including soft-deleted records.
|
||||
///
|
||||
/// Returns `None` if the character does not exist at all.
|
||||
pub async fn get_character_by_token_including_deleted(
|
||||
character_token: &CharacterToken,
|
||||
connection: &mut PoolConnection<MySql>,
|
||||
) -> Result<Option<CharacterOwnershipRecord>, sqlx::Error> {
|
||||
let result = sqlx::query_as::<_, CharacterOwnershipRecord>(
|
||||
r#"
|
||||
SELECT
|
||||
token,
|
||||
maybe_creator_user_token,
|
||||
is_active,
|
||||
(deleted_at IS NOT NULL) as is_deleted
|
||||
FROM characters
|
||||
WHERE token = ?
|
||||
"#,
|
||||
)
|
||||
.bind(character_token.as_str())
|
||||
.fetch_optional(&mut **connection)
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
/// Look up a character token by its Kinovi character ID.
|
||||
pub async fn get_character_token_by_kinovi_id(
|
||||
kinovi_character_id: &str,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<Option<CharacterToken>, sqlx::Error> {
|
||||
let row = sqlx::query_as::<_, (CharacterToken,)>(
|
||||
r#"
|
||||
SELECT token
|
||||
FROM characters
|
||||
WHERE kinovi_character_id = ?
|
||||
AND deleted_at IS NULL
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(kinovi_character_id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(row.map(|(token,)| token))
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::MySql;
|
||||
|
||||
use enums::by_table::characters::character_type::CharacterType;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
const DEFAULT_PAGE_SIZE: u32 = 50;
|
||||
|
||||
/// A character row for the list endpoint.
|
||||
pub struct CharacterListRow {
|
||||
pub id: u64,
|
||||
pub token: CharacterToken,
|
||||
pub character_type: CharacterType,
|
||||
pub is_active: bool,
|
||||
pub character_name: Option<String>,
|
||||
pub maybe_description: Option<String>,
|
||||
pub maybe_avatar_media_token: Option<MediaFileToken>,
|
||||
pub maybe_full_image_media_token: Option<MediaFileToken>,
|
||||
pub maybe_original_upload_media_token: Option<MediaFileToken>,
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub kinovi_character_id: Option<String>,
|
||||
pub kinovi_character_name: Option<String>,
|
||||
pub maybe_kinovi_asset_id: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ListActiveCharactersResult {
|
||||
pub characters: Vec<CharacterListRow>,
|
||||
pub next_cursor: Option<u64>,
|
||||
}
|
||||
|
||||
/// List active, non-deleted characters for a user, paginated by descending id.
|
||||
pub async fn list_active_characters_for_user(
|
||||
user_token: &UserToken,
|
||||
maybe_cursor: Option<u64>,
|
||||
connection: &mut PoolConnection<MySql>,
|
||||
) -> Result<ListActiveCharactersResult, sqlx::Error> {
|
||||
let fetch_limit = (DEFAULT_PAGE_SIZE + 1) as i64;
|
||||
let id_cursor = maybe_cursor.map(|c| c as i64).unwrap_or(i64::MAX);
|
||||
|
||||
let rows = sqlx::query_as!(
|
||||
CharacterListRow,
|
||||
r#"
|
||||
SELECT
|
||||
id as `id: u64`,
|
||||
token as `token: tokens::tokens::characters::CharacterToken`,
|
||||
character_type as `character_type: enums::by_table::characters::character_type::CharacterType`,
|
||||
is_active as `is_active: bool`,
|
||||
character_name,
|
||||
maybe_description,
|
||||
maybe_avatar_media_token as `maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken`,
|
||||
maybe_full_image_media_token as `maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken`,
|
||||
maybe_original_upload_media_token as `maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken`,
|
||||
maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
|
||||
kinovi_character_id,
|
||||
kinovi_character_name,
|
||||
maybe_kinovi_asset_id
|
||||
FROM characters
|
||||
WHERE maybe_creator_user_token = ?
|
||||
AND is_active = true
|
||||
AND deleted_at IS NULL
|
||||
AND id < ?
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
user_token.as_str(),
|
||||
id_cursor,
|
||||
fetch_limit,
|
||||
)
|
||||
.fetch_all(&mut **connection)
|
||||
.await?;
|
||||
|
||||
let has_next = rows.len() as u32 >= DEFAULT_PAGE_SIZE;
|
||||
let characters: Vec<CharacterListRow> = rows.into_iter().take(DEFAULT_PAGE_SIZE as usize).collect();
|
||||
let next_cursor = if has_next {
|
||||
characters.last().map(|c| c.id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ListActiveCharactersResult {
|
||||
characters,
|
||||
next_cursor,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub mod activate_character_with_media;
|
||||
pub mod batch_lookup_characters_by_token_for_prompting;
|
||||
pub mod create_pending_character;
|
||||
pub mod delete_character;
|
||||
pub mod get_character_by_token;
|
||||
pub mod get_character_by_token_including_deleted;
|
||||
pub mod get_character_token_by_kinovi_id;
|
||||
pub mod list_active_characters_for_user;
|
||||
pub mod update_character_name_and_description;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::MySql;
|
||||
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
/// Update a character's name, kinovi name, and/or description.
|
||||
///
|
||||
/// All fields are always SET in the query — the caller should pass the
|
||||
/// existing values for fields that should not change.
|
||||
pub async fn update_character_name_and_description(
|
||||
character_token: &CharacterToken,
|
||||
character_name: &str,
|
||||
kinovi_character_name: &str,
|
||||
maybe_description: Option<&str>,
|
||||
connection: &mut PoolConnection<MySql>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE characters
|
||||
SET
|
||||
character_name = ?,
|
||||
kinovi_character_name = ?,
|
||||
maybe_description = ?
|
||||
WHERE token = ?
|
||||
AND deleted_at IS NULL
|
||||
LIMIT 1
|
||||
"#,
|
||||
character_name,
|
||||
kinovi_character_name,
|
||||
maybe_description,
|
||||
character_token.as_str(),
|
||||
)
|
||||
.execute(&mut **connection)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
use anyhow::anyhow;
|
||||
use log::info;
|
||||
use sqlx::{Executor, MySql};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::common::visibility::Visibility;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::errors::database_query_error::DatabaseQueryError;
|
||||
|
||||
pub struct InsertGenericInferenceForSeedance2ProCharacterWithAprioriJobTokenArgs<'e, 'c, E>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
pub uuid_idempotency_token: &'e str,
|
||||
|
||||
/// Pre-generated job token.
|
||||
pub apriori_job_token: &'e InferenceJobToken,
|
||||
|
||||
/// The Kinovi character_id returned by generate_character().
|
||||
pub kinovi_character_id: &'e str,
|
||||
|
||||
pub maybe_creator_user_token: Option<&'e UserToken>,
|
||||
pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>,
|
||||
pub creator_ip_address: &'e str,
|
||||
pub creator_set_visibility: Visibility,
|
||||
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
pub async fn insert_generic_inference_job_for_seedance2pro_character_with_apriori_job_token<'e, 'c : 'e, E>(
|
||||
args: InsertGenericInferenceForSeedance2ProCharacterWithAprioriJobTokenArgs<'e, 'c, E>
|
||||
) -> Result<InferenceJobToken, DatabaseQueryError>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
const JOB_TYPE: InferenceJobType = InferenceJobType::Seedance2ProCharacter;
|
||||
const INFERENCE_CATEGORY: InferenceCategory = InferenceCategory::CharacterGeneration;
|
||||
const PRODUCT_CATEGORY: InferenceJobProductCategory = InferenceJobProductCategory::Seedance2ProCharacter;
|
||||
const EXTERNAL_THIRD_PARTY: InferenceJobExternalThirdParty = InferenceJobExternalThirdParty::Seedance2Pro;
|
||||
const STATUS: JobStatusPlus = JobStatusPlus::Pending;
|
||||
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO generic_inference_jobs
|
||||
SET
|
||||
token = ?,
|
||||
uuid_idempotency_token = ?,
|
||||
|
||||
job_type = ?,
|
||||
|
||||
maybe_external_third_party = ?,
|
||||
maybe_external_third_party_id = ?,
|
||||
|
||||
product_category = ?,
|
||||
inference_category = ?,
|
||||
|
||||
maybe_model_type = NULL,
|
||||
maybe_model_token = NULL,
|
||||
|
||||
maybe_input_source_token = NULL,
|
||||
maybe_input_source_token_type = NULL,
|
||||
|
||||
maybe_download_url = NULL,
|
||||
maybe_cover_image_media_file_token = NULL,
|
||||
|
||||
maybe_prompt_token = NULL,
|
||||
|
||||
maybe_wallet_ledger_entry_token = NULL,
|
||||
|
||||
maybe_raw_inference_text = NULL,
|
||||
|
||||
maybe_inference_args = NULL,
|
||||
|
||||
maybe_creator_user_token = ?,
|
||||
maybe_creator_anonymous_visitor_token = ?,
|
||||
creator_ip_address = ?,
|
||||
creator_set_visibility = ?,
|
||||
|
||||
priority_level = 0,
|
||||
is_keepalive_required = FALSE,
|
||||
max_duration_seconds = 0,
|
||||
|
||||
is_debug_request = FALSE,
|
||||
maybe_routing_tag = NULL,
|
||||
|
||||
status = ?
|
||||
"#,
|
||||
args.apriori_job_token.as_str(),
|
||||
args.uuid_idempotency_token,
|
||||
|
||||
JOB_TYPE.to_str(),
|
||||
|
||||
EXTERNAL_THIRD_PARTY.to_str(),
|
||||
args.kinovi_character_id,
|
||||
|
||||
PRODUCT_CATEGORY.to_str(),
|
||||
INFERENCE_CATEGORY.to_str(),
|
||||
|
||||
args.maybe_creator_user_token.map(|t| t.to_string()),
|
||||
args.maybe_avt_token.map(|t| t.to_string()),
|
||||
args.creator_ip_address,
|
||||
args.creator_set_visibility.to_str(),
|
||||
|
||||
STATUS.to_str(),
|
||||
);
|
||||
|
||||
let query_result = query.execute(args.mysql_executor)
|
||||
.await;
|
||||
|
||||
let record_id = match query_result {
|
||||
Err(err) => return Err(DatabaseQueryError::from(err)),
|
||||
Ok(res) => res.last_insert_id(),
|
||||
};
|
||||
|
||||
info!("Insert generic inference job for Seedance2Pro character: {} with record ID {}", args.apriori_job_token, record_id);
|
||||
|
||||
Ok(args.apriori_job_token.clone())
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
use log::warn;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// A pending Seedance2Pro character job waiting for a result.
|
||||
#[derive(Debug)]
|
||||
pub struct PendingSeedance2ProCharacterJob {
|
||||
pub job_token: InferenceJobToken,
|
||||
|
||||
/// The Kinovi character_id (stored as the external third party ID).
|
||||
pub kinovi_character_id: String,
|
||||
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct RawRecord {
|
||||
job_token: InferenceJobToken,
|
||||
kinovi_character_id: Option<String>,
|
||||
maybe_creator_user_token: Option<UserToken>,
|
||||
}
|
||||
|
||||
/// Returns all non-terminal Seedance2Pro character jobs that have an associated character ID.
|
||||
pub async fn list_pending_seedance2pro_character_jobs(pool: &MySqlPool) -> Result<Vec<PendingSeedance2ProCharacterJob>, sqlx::Error> {
|
||||
let records = sqlx::query_as!(
|
||||
RawRecord,
|
||||
r#"
|
||||
SELECT
|
||||
jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,
|
||||
jobs.maybe_external_third_party_id as `kinovi_character_id`,
|
||||
jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`
|
||||
|
||||
FROM generic_inference_jobs as jobs
|
||||
|
||||
WHERE jobs.maybe_external_third_party = ?
|
||||
AND jobs.job_type = ?
|
||||
AND jobs.status NOT IN ('complete_success', 'complete_failure')
|
||||
AND jobs.maybe_external_third_party_id IS NOT NULL
|
||||
|
||||
LIMIT 25000
|
||||
"#,
|
||||
InferenceJobExternalThirdParty::Seedance2Pro.to_str(),
|
||||
InferenceJobType::Seedance2ProCharacter.to_str(),
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let jobs = records
|
||||
.into_iter()
|
||||
.filter_map(|record| {
|
||||
let kinovi_character_id = match record.kinovi_character_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
warn!("PendingSeedance2ProCharacterJob has no kinovi_character_id, skipping");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(PendingSeedance2ProCharacterJob {
|
||||
job_token: record.job_token,
|
||||
kinovi_character_id,
|
||||
maybe_creator_user_token: record.maybe_creator_user_token,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(jobs)
|
||||
}
|
||||
+6
-6
@@ -1,10 +1,9 @@
|
||||
use anyhow::anyhow;
|
||||
use log::warn;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
@@ -41,8 +40,8 @@ struct RawRecord {
|
||||
maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
|
||||
}
|
||||
|
||||
/// Returns all non-terminal Seedance2Pro jobs that have an associated order_id.
|
||||
pub async fn list_pending_seedance2pro_jobs(pool: &MySqlPool) -> AnyhowResult<Vec<PendingSeedance2ProJob>> {
|
||||
/// Returns all non-terminal Seedance2Pro video jobs that have an associated order_id.
|
||||
pub async fn list_pending_seedance2pro_video_jobs(pool: &MySqlPool) -> Result<Vec<PendingSeedance2ProJob>, sqlx::Error> {
|
||||
let records = sqlx::query_as!(
|
||||
RawRecord,
|
||||
r#"
|
||||
@@ -59,16 +58,17 @@ SELECT
|
||||
FROM generic_inference_jobs as jobs
|
||||
|
||||
WHERE jobs.maybe_external_third_party = ?
|
||||
AND jobs.job_type = ?
|
||||
AND jobs.status NOT IN ('complete_success', 'complete_failure')
|
||||
AND jobs.maybe_external_third_party_id IS NOT NULL
|
||||
|
||||
LIMIT 25000
|
||||
"#,
|
||||
InferenceJobExternalThirdParty::Seedance2Pro.to_str(),
|
||||
InferenceJobType::Seedance2ProQueue.to_str(),
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error querying pending seedance2pro jobs: {:?}", err))?;
|
||||
.await?;
|
||||
|
||||
let jobs = records
|
||||
.into_iter()
|
||||
+3
-1
@@ -1,2 +1,4 @@
|
||||
pub mod insert_generic_inference_job_for_seedance2pro_character_with_apriori_job_token;
|
||||
pub mod insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token;
|
||||
pub mod list_pending_seedance2pro_jobs;
|
||||
pub mod list_pending_seedance2pro_character_jobs;
|
||||
pub mod list_pending_seedance2pro_video_jobs;
|
||||
|
||||
+2
@@ -187,6 +187,7 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
|
||||
InferenceCategory::SeedVc => Some("Seed VC"),
|
||||
InferenceCategory::ConvertBvhToWorkflow => Some("BVH to Workflow"),
|
||||
InferenceCategory::SplatGeneration => Some("Splat Generation"),
|
||||
InferenceCategory::CharacterGeneration => Some("Character Generation"),
|
||||
InferenceCategory::DeprecatedField => Some("Job"), // TODO(bt,2024-07-16): Fix
|
||||
};
|
||||
}
|
||||
@@ -210,6 +211,7 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
|
||||
InferenceCategory::LivePortrait => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::ConvertBvhToWorkflow => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::SplatGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::CharacterGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::DeprecatedField => (true, None), // TODO(bt,2024-07-16): We'll need to read another field!
|
||||
};
|
||||
|
||||
|
||||
+2
@@ -162,6 +162,7 @@ fn raw_record_to_public_result(record: RawGenericInferenceJobStatus) -> GenericI
|
||||
InferenceCategory::SeedVc => Some("Seed VC"),
|
||||
InferenceCategory::ConvertBvhToWorkflow => Some("BVH to Workflow"),
|
||||
InferenceCategory::SplatGeneration => Some("Splat Generation"),
|
||||
InferenceCategory::CharacterGeneration => Some("Character Generation"),
|
||||
InferenceCategory::DeprecatedField => Some("Job"), // TODO(bt,2024-07-16): Fix
|
||||
};
|
||||
}
|
||||
@@ -185,6 +186,7 @@ fn raw_record_to_public_result(record: RawGenericInferenceJobStatus) -> GenericI
|
||||
InferenceCategory::SeedVc => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::ConvertBvhToWorkflow => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::SplatGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::CharacterGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::DeprecatedField => (true, None), // TODO(bt,2024-07-16): We'll need to read another field!
|
||||
};
|
||||
|
||||
|
||||
+2
@@ -253,6 +253,7 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
|
||||
InferenceCategory::LivePortrait => Some("Live Portrait"),
|
||||
InferenceCategory::SeedVc => Some("Seed VC"),
|
||||
InferenceCategory::SplatGeneration => Some("Splat Generation"),
|
||||
InferenceCategory::CharacterGeneration => Some("Character Generation"),
|
||||
InferenceCategory::DeprecatedField => Some("Job"), // TODO(bt,2024-07-16): Fix
|
||||
};
|
||||
}
|
||||
@@ -276,6 +277,7 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
|
||||
InferenceCategory::SeedVc => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::ConvertBvhToWorkflow => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::SplatGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::CharacterGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
|
||||
InferenceCategory::DeprecatedField => (true, None), // TODO(bt,2024-07-16): We'll need to read another field!
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod batch_generations;
|
||||
pub mod beta_keys;
|
||||
pub mod billing;
|
||||
pub mod browser_session_logs;
|
||||
pub mod characters;
|
||||
pub mod comments;
|
||||
pub mod database_time;
|
||||
pub mod email_sender_jobs;
|
||||
|
||||
+5
@@ -25,6 +25,10 @@ pub struct CreateAccountFromEmailPasswordArgs<'a> {
|
||||
/// If the browser doesn't send this parameter, we'll try the `referer` header.
|
||||
pub maybe_referral_url: Option<String>,
|
||||
|
||||
/// The URL where the user landed when they first arrived, prior to navigation and signing up.
|
||||
/// We can grab this from `window.location.href`.
|
||||
pub maybe_landing_url: Option<String>,
|
||||
|
||||
/// In production code, send this as `None`.
|
||||
/// Only provide an external user token for db integration tests and db seeding tools.
|
||||
/// This allows for knowing the user token a priori.
|
||||
@@ -68,6 +72,7 @@ pub async fn create_account_from_email_and_password(
|
||||
is_without_password: false,
|
||||
|
||||
maybe_referral_url: args.maybe_referral_url,
|
||||
maybe_landing_url: args.maybe_landing_url,
|
||||
|
||||
// NB: This is just for testing.
|
||||
maybe_user_token: args.maybe_user_token,
|
||||
|
||||
+1
@@ -59,6 +59,7 @@ pub async fn create_account_from_google_sso(
|
||||
|
||||
// TODO: Necessary? We're not doing SSO for now.
|
||||
maybe_referral_url: None,
|
||||
maybe_landing_url: None,
|
||||
|
||||
// NB: This is just for testing.
|
||||
maybe_user_token: None,
|
||||
|
||||
+2
@@ -27,6 +27,7 @@ pub struct CreateAccountFromStripeCheckoutArgs<'a> {
|
||||
pub ip_address: &'a str,
|
||||
pub maybe_source: Option<UserSignupSource>,
|
||||
pub maybe_referral_url: Option<String>,
|
||||
pub maybe_landing_url: Option<String>,
|
||||
|
||||
/// Comma separated string of feature flags.
|
||||
pub maybe_feature_flags: Option<&'a str>,
|
||||
@@ -70,6 +71,7 @@ pub async fn create_account_from_stripe_checkout(
|
||||
is_without_password: true,
|
||||
|
||||
maybe_referral_url: args.maybe_referral_url,
|
||||
maybe_landing_url: args.maybe_landing_url,
|
||||
|
||||
// NB: This is just for testing.
|
||||
maybe_user_token: None,
|
||||
|
||||
+19
-2
@@ -65,6 +65,10 @@ pub struct GenericCreateAccountArgs<'a> {
|
||||
/// If the browser doesn't send this parameter, we'll try the `referer` header.
|
||||
pub maybe_referral_url: Option<String>,
|
||||
|
||||
/// The URL where the user landed when they first arrived, prior to navigation and signing up.
|
||||
/// The browser can send `window.location.href` to the backend so we know how people are finding us.
|
||||
pub maybe_landing_url: Option<String>,
|
||||
|
||||
/// In production code, send this as `None`.
|
||||
/// Only provide an external user token for db integration tests and db seeding tools.
|
||||
/// This allows for knowing the user token a priori.
|
||||
@@ -86,6 +90,9 @@ pub async fn create_account_generic(
|
||||
const INITIAL_PROFILE_RENDERED_HTML : &str = "";
|
||||
const INITIAL_USER_ROLE: &str = "user";
|
||||
|
||||
let maybe_referral_url = sanitize_optional_url(args.maybe_referral_url);
|
||||
let maybe_landing_url = sanitize_optional_url(args.maybe_landing_url);
|
||||
|
||||
let user_token = match args.maybe_user_token {
|
||||
None => UserToken::generate(),
|
||||
Some(user_token) => user_token.clone(),
|
||||
@@ -131,7 +138,8 @@ SET
|
||||
|
||||
maybe_signup_method = ?,
|
||||
|
||||
maybe_referral_url = ?
|
||||
maybe_referral_url = ?,
|
||||
maybe_landing_url = ?
|
||||
"#,
|
||||
user_token.as_str(),
|
||||
|
||||
@@ -167,7 +175,8 @@ SET
|
||||
args.maybe_source.map(|s| s.to_str()),
|
||||
args.maybe_signup_method.map(|m| m.to_str()),
|
||||
|
||||
args.maybe_referral_url,
|
||||
maybe_referral_url,
|
||||
maybe_landing_url,
|
||||
);
|
||||
|
||||
|
||||
@@ -207,3 +216,11 @@ SET
|
||||
user_id: record_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Trim, reject empty strings, and truncate to 255 characters.
|
||||
fn sanitize_optional_url(value: Option<String>) -> Option<String> {
|
||||
value
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.chars().take(255).collect())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
|
||||
/// Used in the `characters` table in `VARCHAR(24)` field `character_type`.
|
||||
///
|
||||
/// Identifies the type/backend used to create the character.
|
||||
///
|
||||
/// YOU CAN ADD NEW VALUES, BUT DO NOT CHANGE EXISTING VALUES WITHOUT A MIGRATION STRATEGY.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize)]
|
||||
pub enum CharacterType {
|
||||
/// Character created via Kinovi / Seedance2Pro.
|
||||
#[serde(rename = "kinovi_seedance")]
|
||||
KinoviSeedance,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(CharacterType);
|
||||
impl_mysql_enum_coders!(CharacterType);
|
||||
|
||||
impl CharacterType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::KinoviSeedance => "kinovi_seedance",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"kinovi_seedance" => Ok(Self::KinoviSeedance),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
BTreeSet::from([
|
||||
Self::KinoviSeedance,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::by_table::characters::character_type::CharacterType;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod explicit_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(CharacterType::KinoviSeedance, "kinovi_seedance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(CharacterType::KinoviSeedance.to_str(), "kinovi_seedance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(CharacterType::from_str("kinovi_seedance").unwrap(), CharacterType::KinoviSeedance);
|
||||
assert!(CharacterType::from_str("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
const EXPECTED_COUNT: usize = 1;
|
||||
|
||||
assert_eq!(CharacterType::all_variants().len(), EXPECTED_COUNT);
|
||||
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(CharacterType::all_variants().len(), CharacterType::iter().len());
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(CharacterType::all_variants().len(), CharacterType::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in CharacterType::all_variants() {
|
||||
assert_eq!(variant, CharacterType::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, CharacterType::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, CharacterType::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
const MAX_LENGTH: usize = 24;
|
||||
for variant in CharacterType::all_variants() {
|
||||
let serialized = variant.to_str();
|
||||
assert!(!serialized.is_empty(), "variant {:?} is too short", variant);
|
||||
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long for VARCHAR({})", variant, MAX_LENGTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod character_type;
|
||||
+11
-1
@@ -52,6 +52,10 @@ pub enum InferenceCategory {
|
||||
#[serde(rename = "splat_generation")]
|
||||
SplatGeneration,
|
||||
|
||||
/// Character generation (eg. Kinovi/Seedance2Pro characters)
|
||||
#[serde(rename = "character_generation")]
|
||||
CharacterGeneration,
|
||||
|
||||
/// FAL-powered image background removal
|
||||
#[serde(rename = "background_removal")]
|
||||
BackgroundRemoval,
|
||||
@@ -109,6 +113,7 @@ impl InferenceCategory {
|
||||
Self::VideoGeneration => "video_generation",
|
||||
Self::ObjectGeneration => "object_generation",
|
||||
Self::SplatGeneration => "splat_generation",
|
||||
Self::CharacterGeneration => "character_generation",
|
||||
Self::BackgroundRemoval => "background_removal",
|
||||
Self::Mocap => "mocap",
|
||||
Self::Workflow => "workflow",
|
||||
@@ -131,6 +136,7 @@ impl InferenceCategory {
|
||||
"video_generation" => Ok(Self::VideoGeneration),
|
||||
"object_generation" => Ok(Self::ObjectGeneration),
|
||||
"splat_generation" => Ok(Self::SplatGeneration),
|
||||
"character_generation" => Ok(Self::CharacterGeneration),
|
||||
"background_removal" => Ok(Self::BackgroundRemoval),
|
||||
"f5_tts" => Ok(Self::F5TTS),
|
||||
"mocap" => Ok(Self::Mocap),
|
||||
@@ -155,6 +161,7 @@ impl InferenceCategory {
|
||||
Self::ImageGeneration,
|
||||
Self::ObjectGeneration,
|
||||
Self::SplatGeneration,
|
||||
Self::CharacterGeneration,
|
||||
Self::VideoGeneration,
|
||||
Self::BackgroundRemoval,
|
||||
Self::Mocap,
|
||||
@@ -187,6 +194,7 @@ mod tests {
|
||||
assert_serialization(InferenceCategory::VideoGeneration, "video_generation");
|
||||
assert_serialization(InferenceCategory::ObjectGeneration, "object_generation");
|
||||
assert_serialization(InferenceCategory::SplatGeneration, "splat_generation");
|
||||
assert_serialization(InferenceCategory::CharacterGeneration, "character_generation");
|
||||
assert_serialization(InferenceCategory::BackgroundRemoval, "background_removal");
|
||||
assert_serialization(InferenceCategory::Mocap, "mocap");
|
||||
assert_serialization(InferenceCategory::F5TTS, "f5_tts");
|
||||
@@ -208,6 +216,7 @@ mod tests {
|
||||
assert_eq!(InferenceCategory::VideoGeneration.to_str(), "video_generation");
|
||||
assert_eq!(InferenceCategory::ObjectGeneration.to_str(), "object_generation");
|
||||
assert_eq!(InferenceCategory::SplatGeneration.to_str(), "splat_generation");
|
||||
assert_eq!(InferenceCategory::CharacterGeneration.to_str(), "character_generation");
|
||||
assert_eq!(InferenceCategory::BackgroundRemoval.to_str(), "background_removal");
|
||||
assert_eq!(InferenceCategory::F5TTS.to_str(), "f5_tts");
|
||||
assert_eq!(InferenceCategory::SeedVc.to_str(), "seed_vc");
|
||||
@@ -229,6 +238,7 @@ mod tests {
|
||||
assert_eq!(InferenceCategory::from_str("video_generation").unwrap(), InferenceCategory::VideoGeneration);
|
||||
assert_eq!(InferenceCategory::from_str("object_generation").unwrap(), InferenceCategory::ObjectGeneration);
|
||||
assert_eq!(InferenceCategory::from_str("splat_generation").unwrap(), InferenceCategory::SplatGeneration);
|
||||
assert_eq!(InferenceCategory::from_str("character_generation").unwrap(), InferenceCategory::CharacterGeneration);
|
||||
assert_eq!(InferenceCategory::from_str("background_removal").unwrap(), InferenceCategory::BackgroundRemoval);
|
||||
assert_eq!(InferenceCategory::from_str("f5_tts").unwrap(), InferenceCategory::F5TTS);
|
||||
assert_eq!(InferenceCategory::from_str("seed_vc").unwrap(), InferenceCategory::SeedVc);
|
||||
@@ -243,7 +253,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
const EXPECTED_COUNT : usize = 17;
|
||||
const EXPECTED_COUNT : usize = 18;
|
||||
|
||||
assert_eq!(InferenceCategory::all_variants().len(), EXPECTED_COUNT);
|
||||
assert_eq!(InferenceCategory::iter().len(), EXPECTED_COUNT);
|
||||
|
||||
+10
-1
@@ -35,6 +35,9 @@ pub enum InferenceJobProductCategory {
|
||||
#[serde(rename = "seedance2pro_video")]
|
||||
Seedance2ProVideo,
|
||||
|
||||
#[serde(rename = "seedance2pro_character")]
|
||||
Seedance2ProCharacter,
|
||||
|
||||
// =============== WORLD LABS ===============
|
||||
|
||||
/// World Labs: Gaussian Splat Generation
|
||||
@@ -137,6 +140,7 @@ impl InferenceJobProductCategory {
|
||||
Self::FalObject => "fal_object",
|
||||
Self::FalBgRemoval => "fal_bg_removal",
|
||||
Self::Seedance2ProVideo => "seedance2pro_video",
|
||||
Self::Seedance2ProCharacter => "seedance2pro_character",
|
||||
Self::WorldlabsSplat => "worldlabs_splat",
|
||||
Self::TtsGptSoVits => "tts_gpt_so_vits",
|
||||
Self::TtsStyleTts2 => "tts_style_tts2",
|
||||
@@ -171,6 +175,7 @@ impl InferenceJobProductCategory {
|
||||
"fal_object" => Ok(Self::FalObject),
|
||||
"fal_bg_removal" => Ok(Self::FalBgRemoval),
|
||||
"seedance2pro_video" => Ok(Self::Seedance2ProVideo),
|
||||
"seedance2pro_character" => Ok(Self::Seedance2ProCharacter),
|
||||
"worldlabs_splat" => Ok(Self::WorldlabsSplat),
|
||||
"tts_gpt_so_vits" => Ok(Self::TtsGptSoVits),
|
||||
"tts_style_tts2" => Ok(Self::TtsStyleTts2),
|
||||
@@ -208,6 +213,7 @@ impl InferenceJobProductCategory {
|
||||
Self::FalObject,
|
||||
Self::FalBgRemoval,
|
||||
Self::Seedance2ProVideo,
|
||||
Self::Seedance2ProCharacter,
|
||||
Self::WorldlabsSplat,
|
||||
Self::TtsGptSoVits,
|
||||
Self::TtsStyleTts2,
|
||||
@@ -251,6 +257,7 @@ mod tests {
|
||||
assert_serialization(InferenceJobProductCategory::FalObject, "fal_object");
|
||||
assert_serialization(InferenceJobProductCategory::FalBgRemoval, "fal_bg_removal");
|
||||
assert_serialization(InferenceJobProductCategory::Seedance2ProVideo, "seedance2pro_video");
|
||||
assert_serialization(InferenceJobProductCategory::Seedance2ProCharacter, "seedance2pro_character");
|
||||
assert_serialization(InferenceJobProductCategory::WorldlabsSplat, "worldlabs_splat");
|
||||
assert_serialization(InferenceJobProductCategory::TtsGptSoVits, "tts_gpt_so_vits");
|
||||
assert_serialization(InferenceJobProductCategory::TtsStyleTts2, "tts_style_tts2");
|
||||
@@ -284,6 +291,7 @@ mod tests {
|
||||
assert_eq!(InferenceJobProductCategory::FalObject.to_str(), "fal_object");
|
||||
assert_eq!(InferenceJobProductCategory::FalBgRemoval.to_str(), "fal_bg_removal");
|
||||
assert_eq!(InferenceJobProductCategory::Seedance2ProVideo.to_str(), "seedance2pro_video");
|
||||
assert_eq!(InferenceJobProductCategory::Seedance2ProCharacter.to_str(), "seedance2pro_character");
|
||||
assert_eq!(InferenceJobProductCategory::WorldlabsSplat.to_str(), "worldlabs_splat");
|
||||
assert_eq!(InferenceJobProductCategory::TtsGptSoVits.to_str(), "tts_gpt_so_vits");
|
||||
assert_eq!(InferenceJobProductCategory::TtsStyleTts2.to_str(), "tts_style_tts2");
|
||||
@@ -316,6 +324,7 @@ mod tests {
|
||||
assert_eq!(InferenceJobProductCategory::from_str("fal_object").unwrap(), InferenceJobProductCategory::FalObject);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("fal_bg_removal").unwrap(), InferenceJobProductCategory::FalBgRemoval);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_video").unwrap(), InferenceJobProductCategory::Seedance2ProVideo);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_character").unwrap(), InferenceJobProductCategory::Seedance2ProCharacter);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("worldlabs_splat").unwrap(), InferenceJobProductCategory::WorldlabsSplat);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("tts_gpt_so_vits").unwrap(), InferenceJobProductCategory::TtsGptSoVits);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("tts_style_tts2").unwrap(), InferenceJobProductCategory::TtsStyleTts2);
|
||||
@@ -343,7 +352,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
const EXPECTED_COUNT : usize = 29;
|
||||
const EXPECTED_COUNT : usize = 30;
|
||||
|
||||
assert_eq!(InferenceJobProductCategory::all_variants().len(), EXPECTED_COUNT);
|
||||
assert_eq!(InferenceJobProductCategory::iter().len(), EXPECTED_COUNT);
|
||||
|
||||
+11
-1
@@ -23,6 +23,10 @@ pub enum InferenceJobType {
|
||||
#[serde(rename = "seedance2pro_queue")]
|
||||
Seedance2ProQueue,
|
||||
|
||||
/// Seedance 2 Pro character creation. We poll for results.
|
||||
#[serde(rename = "seedance2pro_character")]
|
||||
Seedance2ProCharacter,
|
||||
|
||||
/// World Labs jobs. We poll for results.
|
||||
#[serde(rename = "worldlabs_queue")]
|
||||
WorldlabsQueue,
|
||||
@@ -119,6 +123,7 @@ impl InferenceJobType {
|
||||
match self {
|
||||
Self::FalQueue => "fal_queue",
|
||||
Self::Seedance2ProQueue => "seedance2pro_queue",
|
||||
Self::Seedance2ProCharacter => "seedance2pro_character",
|
||||
Self::WorldlabsQueue => "worldlabs_queue",
|
||||
Self::VideoRender => "video_render",
|
||||
Self::LivePortrait => "live_portrait",
|
||||
@@ -147,6 +152,7 @@ impl InferenceJobType {
|
||||
match value {
|
||||
"fal_queue" => Ok(Self::FalQueue),
|
||||
"seedance2pro_queue" => Ok(Self::Seedance2ProQueue),
|
||||
"seedance2pro_character" => Ok(Self::Seedance2ProCharacter),
|
||||
"worldlabs_queue" => Ok(Self::WorldlabsQueue),
|
||||
"video_render" => Ok(Self::VideoRender),
|
||||
"live_portrait" => Ok(Self::LivePortrait),
|
||||
@@ -178,6 +184,7 @@ impl InferenceJobType {
|
||||
BTreeSet::from([
|
||||
Self::FalQueue,
|
||||
Self::Seedance2ProQueue,
|
||||
Self::Seedance2ProCharacter,
|
||||
Self::WorldlabsQueue,
|
||||
Self::VideoRender,
|
||||
Self::LivePortrait,
|
||||
@@ -220,6 +227,7 @@ mod tests {
|
||||
fn test_serialization() {
|
||||
assert_serialization(InferenceJobType::FalQueue, "fal_queue");
|
||||
assert_serialization(InferenceJobType::Seedance2ProQueue, "seedance2pro_queue");
|
||||
assert_serialization(InferenceJobType::Seedance2ProCharacter, "seedance2pro_character");
|
||||
assert_serialization(InferenceJobType::WorldlabsQueue, "worldlabs_queue");
|
||||
assert_serialization(InferenceJobType::VideoRender, "video_render");
|
||||
assert_serialization(InferenceJobType::LivePortrait, "live_portrait");
|
||||
@@ -247,6 +255,7 @@ mod tests {
|
||||
fn to_str() {
|
||||
assert_eq!(InferenceJobType::FalQueue.to_str(), "fal_queue");
|
||||
assert_eq!(InferenceJobType::Seedance2ProQueue.to_str(), "seedance2pro_queue");
|
||||
assert_eq!(InferenceJobType::Seedance2ProCharacter.to_str(), "seedance2pro_character");
|
||||
assert_eq!(InferenceJobType::WorldlabsQueue.to_str(), "worldlabs_queue");
|
||||
assert_eq!(InferenceJobType::VideoRender.to_str(), "video_render");
|
||||
assert_eq!(InferenceJobType::LivePortrait.to_str(), "live_portrait");
|
||||
@@ -274,6 +283,7 @@ mod tests {
|
||||
fn from_str() {
|
||||
assert_eq!(InferenceJobType::from_str("fal_queue").unwrap(), InferenceJobType::FalQueue);
|
||||
assert_eq!(InferenceJobType::from_str("seedance2pro_queue").unwrap(), InferenceJobType::Seedance2ProQueue);
|
||||
assert_eq!(InferenceJobType::from_str("seedance2pro_character").unwrap(), InferenceJobType::Seedance2ProCharacter);
|
||||
assert_eq!(InferenceJobType::from_str("worldlabs_queue").unwrap(), InferenceJobType::WorldlabsQueue);
|
||||
assert_eq!(InferenceJobType::from_str("video_render").unwrap(), InferenceJobType::VideoRender);
|
||||
assert_eq!(InferenceJobType::from_str("live_portrait").unwrap(), InferenceJobType::LivePortrait);
|
||||
@@ -300,7 +310,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
const EXPECTED_COUNT : usize = 23;
|
||||
const EXPECTED_COUNT : usize = 24;
|
||||
|
||||
assert_eq!(InferenceJobType::all_variants().len(), EXPECTED_COUNT);
|
||||
assert_eq!(InferenceJobType::iter().len(), EXPECTED_COUNT);
|
||||
|
||||
+14
-2
@@ -31,7 +31,11 @@ pub enum InferenceResultType {
|
||||
ZeroShotVoiceEmbedding,
|
||||
|
||||
#[serde(rename = "upload_model")]
|
||||
UploadModel
|
||||
UploadModel,
|
||||
|
||||
/// Result is a character record.
|
||||
#[serde(rename = "character")]
|
||||
Character,
|
||||
}
|
||||
|
||||
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
|
||||
@@ -47,6 +51,7 @@ impl InferenceResultType {
|
||||
Self::VoiceConversion => "voice_conversion",
|
||||
Self::ZeroShotVoiceEmbedding => "zs_voice_embedding",
|
||||
Self::UploadModel => "upload_model",
|
||||
Self::Character => "character",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +62,7 @@ impl InferenceResultType {
|
||||
"voice_conversion" => Ok(Self::VoiceConversion),
|
||||
"zs_voice_embedding" => Ok(Self::ZeroShotVoiceEmbedding),
|
||||
"upload_model" => Ok(Self::UploadModel),
|
||||
"character" => Ok(Self::Character),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
@@ -70,6 +76,7 @@ impl InferenceResultType {
|
||||
InferenceResultType::VoiceConversion,
|
||||
InferenceResultType::ZeroShotVoiceEmbedding,
|
||||
InferenceResultType::UploadModel,
|
||||
InferenceResultType::Character,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -86,6 +93,7 @@ mod tests {
|
||||
assert_serialization(InferenceResultType::VoiceConversion, "voice_conversion");
|
||||
assert_serialization(InferenceResultType::ZeroShotVoiceEmbedding, "zs_voice_embedding");
|
||||
assert_serialization(InferenceResultType::UploadModel, "upload_model");
|
||||
assert_serialization(InferenceResultType::Character, "character");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -95,6 +103,7 @@ mod tests {
|
||||
assert_eq!(InferenceResultType::VoiceConversion.to_str(), "voice_conversion");
|
||||
assert_eq!(InferenceResultType::ZeroShotVoiceEmbedding.to_str(), "zs_voice_embedding");
|
||||
assert_eq!(InferenceResultType::UploadModel.to_str(), "upload_model");
|
||||
assert_eq!(InferenceResultType::Character.to_str(), "character");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -104,18 +113,21 @@ mod tests {
|
||||
assert_eq!(InferenceResultType::from_str("voice_conversion").unwrap(), InferenceResultType::VoiceConversion);
|
||||
assert_eq!(InferenceResultType::from_str("zs_voice_embedding").unwrap(), InferenceResultType::ZeroShotVoiceEmbedding);
|
||||
assert_eq!(InferenceResultType::from_str("upload_model").unwrap(), InferenceResultType::UploadModel);
|
||||
assert_eq!(InferenceResultType::from_str("character").unwrap(), InferenceResultType::Character);
|
||||
assert_eq!(InferenceResultType::from_str("invalid").is_err(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
let mut variants = InferenceResultType::all_variants();
|
||||
assert_eq!(variants.len(), 5);
|
||||
assert_eq!(variants.len(), 6);
|
||||
assert_eq!(variants.pop_first(), Some(InferenceResultType::MediaFile));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceResultType::TextToSpeech));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceResultType::VoiceConversion));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceResultType::ZeroShotVoiceEmbedding));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceResultType::UploadModel));
|
||||
assert_eq!(variants.pop_first(), Some(InferenceResultType::Character));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
|
||||
// Generated check
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// ===== MySql =====
|
||||
pub mod audit_logs;
|
||||
pub mod batch_generations;
|
||||
pub mod characters;
|
||||
pub mod beta_keys;
|
||||
pub mod comments;
|
||||
pub mod email_sender_jobs;
|
||||
|
||||
+6
@@ -105,6 +105,10 @@ pub enum PublicInferenceJobType {
|
||||
#[serde(rename = "seedance2pro_queue")]
|
||||
Seedance2ProQueue,
|
||||
|
||||
/// Seedance 2 Pro character generation jobs. We poll for results.
|
||||
#[serde(rename = "seedance2pro_character")]
|
||||
Seedance2ProCharacter,
|
||||
|
||||
/// World Labs splat generation jobs. We poll for results.
|
||||
#[serde(rename = "worldlabs_queue")]
|
||||
WorldlabsQueue,
|
||||
@@ -139,6 +143,7 @@ impl PublicInferenceJobType {
|
||||
InferenceJobType::RerenderAVideo => Self::RerenderAVideo,
|
||||
InferenceJobType::ImageGenApi => Self::ImageGenApi,
|
||||
InferenceJobType::Seedance2ProQueue => Self::Seedance2ProQueue,
|
||||
InferenceJobType::Seedance2ProCharacter => Self::Seedance2ProCharacter,
|
||||
InferenceJobType::WorldlabsQueue => Self::WorldlabsQueue,
|
||||
}
|
||||
}
|
||||
@@ -164,6 +169,7 @@ impl PublicInferenceJobType {
|
||||
Self::StableDiffusion => InferenceJobType::StableDiffusion,
|
||||
Self::ImageGenApi => InferenceJobType::ImageGenApi,
|
||||
Self::Seedance2ProQueue => InferenceJobType::Seedance2ProQueue,
|
||||
Self::Seedance2ProCharacter => InferenceJobType::Seedance2ProCharacter,
|
||||
Self::WorldlabsQueue => InferenceJobType::WorldlabsQueue,
|
||||
Self::StyleTTS2 => InferenceJobType::StyleTTS2,
|
||||
Self::Tacotron2 => InferenceJobType::Tacotron2,
|
||||
|
||||
@@ -58,7 +58,12 @@ pub fn run_http_server(args: CreateServerArgs) -> AnyhowResult<Server> {
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
)
|
||||
})
|
||||
.bind(bind_address)?
|
||||
.bind(&bind_address)
|
||||
.unwrap_or_else(|err| {
|
||||
eprintln!("FATAL: Failed to bind to address '{}': {}", bind_address, err);
|
||||
eprintln!("The address is likely already in use by another process.");
|
||||
std::process::exit(1);
|
||||
})
|
||||
.workers(num_workers)
|
||||
.run();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use server_environment::ServerEnvironment;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobDependencies {
|
||||
pub mysql_pool: MySqlPool,
|
||||
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use log::{error, info, warn};
|
||||
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_character_jobs::list_pending_seedance2pro_character_jobs;
|
||||
use seedance2pro_client::requests::poll_characters::poll_characters::{
|
||||
poll_characters, CharacterCreationStatus, CharacterStatus, PollCharactersArgs,
|
||||
};
|
||||
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
use super::process_failed_character::process_failed_character;
|
||||
use super::process_successful_character::process_successful_character;
|
||||
|
||||
const POLL_ALERT_THRESHOLD: Duration = Duration::from_secs(600);
|
||||
|
||||
pub async fn character_polling_main_loop(deps: JobDependencies) {
|
||||
while !deps.application_shutdown.get() {
|
||||
let start = Instant::now();
|
||||
let result = run_poll_iteration(&deps).await;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Error in character poll iteration: {:?}", err);
|
||||
let _ = deps.job_stats.increment_failure_count();
|
||||
}
|
||||
|
||||
if elapsed > POLL_ALERT_THRESHOLD {
|
||||
warn!("Character poll iteration took {:.1}s (threshold: {}s)", elapsed.as_secs_f64(), POLL_ALERT_THRESHOLD.as_secs());
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(deps.poll_interval_millis)).await;
|
||||
}
|
||||
|
||||
warn!("Character polling main loop is shut down.");
|
||||
}
|
||||
|
||||
async fn run_poll_iteration(deps: &JobDependencies) -> anyhow::Result<()> {
|
||||
// 1. Query all pending character creation jobs from DB.
|
||||
let pending_jobs = list_pending_seedance2pro_character_jobs(&deps.mysql_pool).await?;
|
||||
|
||||
if pending_jobs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Found {} pending character job(s).", pending_jobs.len());
|
||||
|
||||
// Build a lookup: kinovi_character_id -> job
|
||||
let job_by_character_id: HashMap<String, _> = pending_jobs
|
||||
.into_iter()
|
||||
.map(|job| (job.kinovi_character_id.clone(), job))
|
||||
.collect();
|
||||
|
||||
// 2. Poll characters from Kinovi API.
|
||||
let response = poll_characters(PollCharactersArgs {
|
||||
session: &deps.seedance2pro_session,
|
||||
cursor: None,
|
||||
limit: None,
|
||||
host_override: None,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("Error polling characters from Kinovi: {:?}", err))?;
|
||||
|
||||
info!("Polled {} character(s) from Kinovi.", response.characters.len());
|
||||
|
||||
// 3. Match polled characters against pending jobs.
|
||||
for character in &response.characters {
|
||||
if deps.application_shutdown.get() {
|
||||
break;
|
||||
}
|
||||
|
||||
let job = match job_by_character_id.get(&character.character_id) {
|
||||
Some(j) => j,
|
||||
None => continue, // Not one of our pending jobs.
|
||||
};
|
||||
|
||||
match &character.status {
|
||||
CharacterCreationStatus::Success => {
|
||||
info!(
|
||||
"Character {} completed successfully, processing job {}",
|
||||
character.character_id, job.job_token,
|
||||
);
|
||||
match process_successful_character(deps, job, character).await {
|
||||
Ok(()) => {
|
||||
let _ = deps.job_stats.increment_success_count();
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Error processing completed character {}: {:?}",
|
||||
character.character_id, err,
|
||||
);
|
||||
let _ = deps.job_stats.increment_failure_count();
|
||||
}
|
||||
}
|
||||
}
|
||||
CharacterCreationStatus::Failed => {
|
||||
info!(
|
||||
"Character {} failed, processing job {}",
|
||||
character.character_id, job.job_token,
|
||||
);
|
||||
process_failed_character(deps, job, character).await;
|
||||
}
|
||||
CharacterCreationStatus::Pending => {
|
||||
// Still in progress — check again next poll.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod character_polling_main_loop;
|
||||
mod process_successful_character;
|
||||
mod process_failed_character;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
use log::{error, info, warn};
|
||||
|
||||
use mysql_queries::queries::generic_inference::job::mark_job_failed_by_token::{mark_job_failed_by_token, MarkJobFailedByTokenArgs};
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_character_jobs::PendingSeedance2ProCharacterJob;
|
||||
use seedance2pro_client::requests::poll_characters::poll_characters::CharacterStatus;
|
||||
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
|
||||
pub async fn process_failed_character(
|
||||
deps: &JobDependencies,
|
||||
job: &PendingSeedance2ProCharacterJob,
|
||||
character: &CharacterStatus,
|
||||
) {
|
||||
let reason = character.fail_reason
|
||||
.as_deref()
|
||||
.unwrap_or("unknown failure reason");
|
||||
|
||||
warn!(
|
||||
"Character {} (job {}) failed: {}",
|
||||
character.character_id, job.job_token.as_str(), reason,
|
||||
);
|
||||
|
||||
let internal_reason = format!(
|
||||
"Kinovi character creation failed. character_id={}, raw_task_status={}, raw_asset_status={:?}, fail_reason={:?}",
|
||||
character.character_id,
|
||||
character.raw_task_status,
|
||||
character.raw_asset_status,
|
||||
character.fail_reason,
|
||||
);
|
||||
|
||||
if let Err(err) = mark_job_failed_by_token(MarkJobFailedByTokenArgs {
|
||||
pool: &deps.mysql_pool,
|
||||
job_token: &job.job_token,
|
||||
maybe_public_failure_reason: Some(reason),
|
||||
internal_debugging_failure_reason: &internal_reason,
|
||||
maybe_frontend_failure_category: None,
|
||||
}).await {
|
||||
error!(
|
||||
"Error marking character job {} as failed: {:?}",
|
||||
job.job_token.as_str(), err,
|
||||
);
|
||||
} else {
|
||||
info!("Marked character job {} as failed.", job.job_token.as_str());
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
use anyhow::anyhow;
|
||||
use log::{error, info, warn};
|
||||
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::generic_inference_jobs::inference_result_type::InferenceResultType;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use errors::AnyhowResult;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use mysql_queries::queries::characters::activate_character_with_media::activate_character_with_media;
|
||||
use mysql_queries::queries::characters::get_character_token_by_kinovi_id::get_character_token_by_kinovi_id;
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_character_jobs::PendingSeedance2ProCharacterJob;
|
||||
use mysql_queries::queries::generic_inference::web::mark_generic_inference_job_successfully_done_by_token::mark_generic_inference_job_successfully_done_by_token;
|
||||
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
|
||||
use seedance2pro_client::requests::poll_characters::poll_characters::CharacterStatus;
|
||||
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
|
||||
const PREFIX: &str = "artcraft_";
|
||||
const SUFFIX_PNG: &str = ".png";
|
||||
const SUFFIX_JPG: &str = ".jpg";
|
||||
|
||||
/// Process a successfully completed character creation.
|
||||
///
|
||||
/// 1. Download result images and upload them as media files.
|
||||
/// 2. Activate the character with the new media tokens.
|
||||
/// 3. Mark the inference job as completed.
|
||||
pub async fn process_successful_character(
|
||||
deps: &JobDependencies,
|
||||
job: &PendingSeedance2ProCharacterJob,
|
||||
character: &CharacterStatus,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
info!(
|
||||
"Processing successful character {} for job {}",
|
||||
character.character_id, job.job_token.as_str(),
|
||||
);
|
||||
|
||||
// --- Look up our character record ---
|
||||
|
||||
let character_token = get_character_token_by_kinovi_id(&character.character_id, &deps.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| anyhow!("Error looking up character by kinovi_id: {:?}", err))?
|
||||
.ok_or_else(|| anyhow!("Character not found for kinovi_id: {}", character.character_id))?;
|
||||
|
||||
// --- Download and upload result images ---
|
||||
|
||||
// The first result image with type "avatar" is the avatar; otherwise use the first image.
|
||||
// The first result image with type "full" or the second image is the full image.
|
||||
let mut maybe_avatar_media_token = None;
|
||||
let mut maybe_full_image_media_token = None;
|
||||
|
||||
for (i, result_image) in character.result_images.iter().enumerate() {
|
||||
let is_avatar = result_image.image_type.as_deref() == Some("avatar") || (i == 0 && maybe_avatar_media_token.is_none());
|
||||
let is_full = result_image.image_type.as_deref() == Some("full") || (i == 1 && maybe_full_image_media_token.is_none());
|
||||
|
||||
if !is_avatar && !is_full {
|
||||
continue;
|
||||
}
|
||||
|
||||
let media_token = match download_and_create_media_file(deps, job, &result_image.url).await {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error processing result image {}: {:?}", result_image.url, err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if is_avatar && maybe_avatar_media_token.is_none() {
|
||||
maybe_avatar_media_token = Some(media_token);
|
||||
} else if is_full && maybe_full_image_media_token.is_none() {
|
||||
maybe_full_image_media_token = Some(media_token);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Activate character ---
|
||||
|
||||
activate_character_with_media(
|
||||
&character_token,
|
||||
maybe_avatar_media_token.as_ref(),
|
||||
maybe_full_image_media_token.as_ref(),
|
||||
character.asset_id.as_deref(),
|
||||
&deps.mysql_pool,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| anyhow!("Error activating character: {:?}", err))?;
|
||||
|
||||
info!("Activated character {} (token={})", character.character_id, character_token);
|
||||
|
||||
// --- Mark job as completed ---
|
||||
|
||||
mark_generic_inference_job_successfully_done_by_token(
|
||||
&deps.mysql_pool,
|
||||
&job.job_token,
|
||||
Some(InferenceResultType::Character),
|
||||
Some(character_token.as_str()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| anyhow!("Error marking job as done: {:?}", err))?;
|
||||
|
||||
info!("Marked job {} as completed.", job.job_token.as_str());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// =============== Private helpers ===============
|
||||
|
||||
/// Download an image from a URL, upload to bucket, and create a media file record.
|
||||
async fn download_and_create_media_file(
|
||||
deps: &JobDependencies,
|
||||
job: &PendingSeedance2ProCharacterJob,
|
||||
image_url: &str,
|
||||
) -> AnyhowResult<tokens::tokens::media_files::MediaFileToken> {
|
||||
info!("Downloading character image: {}", image_url);
|
||||
|
||||
let image_bytes: Vec<u8> = reqwest::get(image_url)
|
||||
.await
|
||||
.map_err(|err| anyhow!("reqwest error downloading image: {:?}", err))?
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|err| anyhow!("error reading image bytes: {:?}", err))?
|
||||
.to_vec();
|
||||
|
||||
info!("Downloaded {} bytes from {}", image_bytes.len(), image_url);
|
||||
|
||||
let checksum = sha256_hash_bytes(&image_bytes)
|
||||
.map_err(|err| anyhow!("error hashing image: {:?}", err))?;
|
||||
|
||||
// Determine extension from URL or default to png.
|
||||
let suffix = if image_url.contains(".jpg") || image_url.contains(".jpeg") {
|
||||
SUFFIX_JPG
|
||||
} else {
|
||||
SUFFIX_PNG
|
||||
};
|
||||
|
||||
let media_type = if suffix == SUFFIX_JPG { MediaFileType::Jpg } else { MediaFileType::Png };
|
||||
let mime_type = if suffix == SUFFIX_JPG { "image/jpeg" } else { "image/png" };
|
||||
|
||||
let bucket_path = MediaFileBucketPath::generate_new(Some(PREFIX), Some(suffix));
|
||||
let object_path = bucket_path.get_full_object_path_str();
|
||||
|
||||
info!("Uploading character image to bucket: {}", object_path);
|
||||
|
||||
deps.public_bucket_client
|
||||
.upload_file_with_content_type_process(object_path, &image_bytes, mime_type)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error uploading image to bucket: {:?}", err))?;
|
||||
|
||||
let media_file_token = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
.media_file_class(MediaFileClass::Image)
|
||||
.media_file_type(media_type)
|
||||
.is_intermediate_system_file(true)
|
||||
.media_file_origin_category(MediaFileOriginCategory::Inference)
|
||||
.media_file_origin_product_category(MediaFileOriginProductCategory::VideoGeneration)
|
||||
.mime_type(mime_type)
|
||||
.file_size_bytes(image_bytes.len() as u64)
|
||||
.checksum_sha2(&checksum)
|
||||
.public_bucket_directory_hash(&bucket_path)
|
||||
.insert_pool(&deps.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error inserting media file record: {:?}", err))?;
|
||||
|
||||
info!("Created media file {} for character image", media_file_token.as_str());
|
||||
|
||||
Ok(media_file_token)
|
||||
}
|
||||
@@ -1,4 +1,2 @@
|
||||
pub mod alert_on_error;
|
||||
pub mod main_loop;
|
||||
pub mod process_job;
|
||||
pub mod process_page_batch;
|
||||
pub mod character_polling_job;
|
||||
pub mod video_polling_job;
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
use log::error;
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_jobs::PendingSeedance2ProJob;
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_video_jobs::PendingSeedance2ProJob;
|
||||
use pager::client::pager::Pager;
|
||||
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
|
||||
use pager::notification::notification_urgency::NotificationUrgency;
|
||||
@@ -13,7 +13,7 @@ pub fn alert_pager_and_return_err<T>(
|
||||
) -> anyhow::Result<T> {
|
||||
let err_message = format!("{:#}", err);
|
||||
|
||||
let mut builder = NotificationDetailsBuilder::from_error(err.into())
|
||||
let mut builder = NotificationDetailsBuilder::from_boxed_error(err.into())
|
||||
.set_title(title.to_string())
|
||||
.set_urgency(Some(NotificationUrgency::Medium));
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod alert_on_error;
|
||||
pub mod video_polling_main_loop;
|
||||
pub mod process_job;
|
||||
pub mod process_page_batch;
|
||||
+3
-3
@@ -2,7 +2,7 @@ use log::{error, info, warn};
|
||||
|
||||
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
|
||||
use mysql_queries::queries::generic_inference::job::mark_job_failed_by_token::{mark_job_failed_by_token, MarkJobFailedByTokenArgs};
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_jobs::PendingSeedance2ProJob;
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_video_jobs::PendingSeedance2ProJob;
|
||||
use mysql_queries::queries::wallets::refund::try_to_refund_ledger_entry::{try_to_refund_ledger_entry, WalletRefundOutcome};
|
||||
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
|
||||
use pager::notification::notification_urgency::NotificationUrgency;
|
||||
@@ -85,7 +85,7 @@ pub async fn process_failed_job(
|
||||
err,
|
||||
);
|
||||
|
||||
let notification = NotificationDetailsBuilder::from_error(err.into())
|
||||
let notification = NotificationDetailsBuilder::from_boxed_error(err.into())
|
||||
.set_title("Seedance2Pro refund failed".to_string())
|
||||
.set_inference_job_token(Some(job.job_token.to_string()))
|
||||
.set_third_party_id(Some(job.order_id.to_string()))
|
||||
@@ -140,7 +140,7 @@ pub async fn process_failed_job(
|
||||
err
|
||||
);
|
||||
|
||||
let notification = NotificationDetailsBuilder::from_error(err.into())
|
||||
let notification = NotificationDetailsBuilder::from_boxed_error(err.into())
|
||||
.set_title("Seedance2Pro mark job failed error".to_string())
|
||||
.set_urgency(Some(NotificationUrgency::Medium))
|
||||
.build();
|
||||
+2
-2
@@ -9,12 +9,12 @@ use enums::by_table::media_files::media_file_origin_product_category::MediaFileO
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use errors::AnyhowResult;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_jobs::PendingSeedance2ProJob;
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_video_jobs::PendingSeedance2ProJob;
|
||||
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
|
||||
use mysql_queries::queries::generic_inference::web::mark_generic_inference_job_successfully_done_by_token::mark_generic_inference_job_successfully_done_by_token;
|
||||
use seedance2pro_client::requests::poll_orders::poll_orders::OrderStatus;
|
||||
|
||||
use crate::jobs::alert_on_error::alert_pager_and_return_err;
|
||||
use crate::jobs::video_polling_job::alert_on_error::alert_pager_and_return_err;
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
|
||||
const PREFIX: &str = "artcraft_";
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use log::{info, warn};
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_jobs::PendingSeedance2ProJob;
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_video_jobs::PendingSeedance2ProJob;
|
||||
use seedance2pro_client::requests::poll_orders::poll_orders::{OrderStatus, TaskStatus};
|
||||
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
use crate::jobs::process_job::process_failed_job::process_failed_job;
|
||||
use crate::jobs::process_job::process_successful_job::process_successful_job;
|
||||
use crate::jobs::video_polling_job::process_job::process_failed_job::process_failed_job;
|
||||
use crate::jobs::video_polling_job::process_job::process_successful_job::process_successful_job;
|
||||
|
||||
/// Process a batch of polled orders against the pending jobs map.
|
||||
///
|
||||
+5
-5
@@ -4,16 +4,16 @@ use std::time::{Duration, Instant};
|
||||
use log::{error, info, warn};
|
||||
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
|
||||
use pager::notification::notification_urgency::NotificationUrgency;
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_jobs::list_pending_seedance2pro_jobs;
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_video_jobs::list_pending_seedance2pro_video_jobs;
|
||||
use seedance2pro_client::requests::poll_orders::poll_orders::{poll_orders, OrderStatus, PollOrdersArgs, TaskStatus};
|
||||
|
||||
use crate::jobs::alert_on_error::alert_pager_and_return_err;
|
||||
use crate::jobs::process_page_batch::process_page_batch;
|
||||
use crate::jobs::video_polling_job::alert_on_error::alert_pager_and_return_err;
|
||||
use crate::jobs::video_polling_job::process_page_batch::process_page_batch;
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
|
||||
const POLL_ALERT_THRESHOLD: Duration = Duration::from_secs(600);
|
||||
|
||||
pub async fn main_loop(job_dependencies: JobDependencies) {
|
||||
pub async fn video_polling_main_loop(job_dependencies: JobDependencies) {
|
||||
while !job_dependencies.application_shutdown.get() {
|
||||
let start = Instant::now();
|
||||
let result = run_poll_iteration(&job_dependencies).await;
|
||||
@@ -50,7 +50,7 @@ pub async fn main_loop(job_dependencies: JobDependencies) {
|
||||
|
||||
async fn run_poll_iteration(deps: &JobDependencies) -> anyhow::Result<()> {
|
||||
// 1. Query all non-terminal Seedance2Pro jobs from DB.
|
||||
let pending_jobs = match list_pending_seedance2pro_jobs(&deps.mysql_pool).await {
|
||||
let pending_jobs = match list_pending_seedance2pro_video_jobs(&deps.mysql_pool).await {
|
||||
Ok(jobs) => jobs,
|
||||
Err(err) => {
|
||||
error!("Failed to list pending seedance2pro jobs: {:?}", err);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user