mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 01:12:40 -04:00
Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aa1e762d5 | ||
|
|
28a0725f34 | ||
|
|
b68d24040f | ||
|
|
3792cf82f2 | ||
|
|
f73add81ef | ||
|
|
7465da6092 | ||
|
|
fc5fda42cf | ||
|
|
0eb4ceed70 | ||
|
|
6f1045e58a | ||
|
|
2b97694ebf | ||
|
|
0f6998cb6b | ||
|
|
8076d1b830 | ||
|
|
9842734ce7 | ||
|
|
72761e11f3 | ||
|
|
60c97ff312 | ||
|
|
03ed457cb5 | ||
|
|
e89229896f | ||
|
|
56c1b57cce | ||
|
|
122c1d6688 | ||
|
|
6768208af3 | ||
|
|
99b5a9774a | ||
|
|
f55a4c722e | ||
|
|
f337d630e3 | ||
|
|
b4e1e3ea57 | ||
|
|
71fd694368 | ||
|
|
f2c386dcb9 | ||
|
|
ffb8cbe433 | ||
|
|
5f062c4512 | ||
|
|
2442cda22f | ||
|
|
0a534921ce | ||
|
|
1fecc4405c | ||
|
|
cbca7e3cb4 | ||
|
|
d107b78498 | ||
|
|
702bdc8777 | ||
|
|
4fe36781de | ||
|
|
029375052d | ||
|
|
72d680411f | ||
|
|
79a7443baa | ||
|
|
004a5c157a | ||
|
|
63d3d63709 | ||
|
|
d6e23838ce | ||
|
|
6785db1d59 | ||
|
|
c5f62daa2b | ||
|
|
7122e4507f |
@@ -5,7 +5,7 @@
|
||||
- Two spaces for indentation (not four)
|
||||
- No minimum supported Rust version; use latest stable features freely
|
||||
- `#[macro_use] extern crate serde_derive;` is used in binary crates; library crates use `use serde_derive::{Serialize, Deserialize};`
|
||||
- Prefer `log` crate macros (`info!`, `warn!`, `error!`) over `println!`
|
||||
- Never use `println!` or `eprintln!` outside of tests; use `log` crate macros (`info!`, `warn!`, `error!`, `debug!`, `trace!`) instead
|
||||
- Use `anyhow::Result` / `AnyhowResult` for fallible functions in application code
|
||||
- Enum Display/Debug: use `impl_enum_display_and_debug_using_to_str!` macro, not `derive_more::Display` (CLion doesn't understand it)
|
||||
- Fields that are optional use the `maybe_` prefix: `maybe_creator_user_token`, `maybe_prompt_token`
|
||||
@@ -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
|
||||
|
||||
@@ -48,6 +51,10 @@ then the story, then the fine print.
|
||||
- Group imports: std, external crates, internal workspace crates, `crate::` imports
|
||||
- Use fully qualified paths for one-off references; `use` for repeated references
|
||||
- Prefer specific imports over wildcards, except in `api_doc.rs` and test modules
|
||||
- When two crates export a type with the same name, alias with a suffix describing the source crate:
|
||||
`use enums::...::CommonResolution as CommonResolutionEnum;`
|
||||
`use artcraft_router::...::CommonResolution as CommonResolutionRouter;`
|
||||
This avoids long fully-qualified paths in function signatures and bodies.
|
||||
|
||||
## TypeScript / Frontend
|
||||
|
||||
|
||||
+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"
|
||||
}
|
||||
@@ -60,6 +60,8 @@ artcraft/
|
||||
- Actix-web for HTTP services
|
||||
- SQLx for MySQL and SQLite
|
||||
- A mix of wreq and reqwest for Rust HTTP clients
|
||||
- Never use `println!` or `eprintln!` outside of tests; use `log` crate macros instead
|
||||
- When two crates export the same type name, alias imports with a suffix: `use foo::Bar as BarFoo;`
|
||||
- TypeScript with Nx, React, Vite, Zustand, and Three.js
|
||||
- Use two spaces for indentation
|
||||
|
||||
|
||||
Generated
+16
-39
@@ -508,7 +508,6 @@ version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"config",
|
||||
"datetimes",
|
||||
"dotenv",
|
||||
"easyenv",
|
||||
@@ -523,6 +522,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"tokens",
|
||||
"tokio",
|
||||
@@ -727,7 +727,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"enums",
|
||||
"enums_public",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -748,7 +747,6 @@ dependencies = [
|
||||
"cloudflare_errors",
|
||||
"cookie 0.18.1",
|
||||
"enums",
|
||||
"enums_public",
|
||||
"errors",
|
||||
"log",
|
||||
"mimetypes",
|
||||
@@ -3133,7 +3131,6 @@ dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.60",
|
||||
"cloud_storage",
|
||||
"config",
|
||||
"easyenv",
|
||||
"elasticsearch",
|
||||
"enums",
|
||||
@@ -3144,6 +3141,7 @@ dependencies = [
|
||||
"mimetypes",
|
||||
"mysql_queries",
|
||||
"rand 0.9.2",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"storyteller_root",
|
||||
"tokens",
|
||||
@@ -3159,7 +3157,6 @@ dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.60",
|
||||
"cloud_storage",
|
||||
"config",
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
@@ -3167,6 +3164,7 @@ dependencies = [
|
||||
"log",
|
||||
"mimetypes",
|
||||
"mysql_queries",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"tokens",
|
||||
"tokio",
|
||||
@@ -3438,6 +3436,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"storyteller_root",
|
||||
"subprocess",
|
||||
@@ -3445,6 +3444,7 @@ dependencies = [
|
||||
"tempdir",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"url",
|
||||
"workspace-build-acceleration",
|
||||
"zip 0.6.6",
|
||||
]
|
||||
@@ -3610,7 +3610,6 @@ version = "0.0.1"
|
||||
dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.60",
|
||||
"config",
|
||||
"easyenv",
|
||||
"elasticsearch",
|
||||
"elasticsearch_schema",
|
||||
@@ -3622,6 +3621,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"storyteller_root",
|
||||
"strum",
|
||||
@@ -3701,6 +3701,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"server_environment",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
@@ -3794,20 +3795,6 @@ dependencies = [
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enums_public"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"enums",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"toml 1.0.7+spec-1.1.0",
|
||||
"utoipa",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env"
|
||||
version = "1.0.1"
|
||||
@@ -3904,7 +3891,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bootstrap",
|
||||
"chrono",
|
||||
"config",
|
||||
"dotenv",
|
||||
"easyenv",
|
||||
"elasticsearch",
|
||||
@@ -3923,6 +3909,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"server_environment",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"storyteller_root",
|
||||
"tokens",
|
||||
@@ -5764,6 +5751,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"subprocess",
|
||||
"subprocess_common",
|
||||
@@ -6101,7 +6089,6 @@ dependencies = [
|
||||
name = "jwt_signer"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"errors",
|
||||
"hmac 0.10.1",
|
||||
"jwt",
|
||||
"sha2 0.9.9",
|
||||
@@ -6146,13 +6133,13 @@ dependencies = [
|
||||
name = "kubectl-pod-cleanup"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"config",
|
||||
"easyenv",
|
||||
"errors",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"shared_env_var_config",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -6856,7 +6843,6 @@ dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.60",
|
||||
"cloud_storage",
|
||||
"config",
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
@@ -6867,6 +6853,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"storyteller_root",
|
||||
"strum",
|
||||
@@ -8657,16 +8644,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prompt_engineering"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"regex",
|
||||
"speculoos",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.12.6"
|
||||
@@ -10197,7 +10174,6 @@ dependencies = [
|
||||
"bucket_paths",
|
||||
"cloud_storage",
|
||||
"concurrency",
|
||||
"config",
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
@@ -11257,7 +11233,6 @@ dependencies = [
|
||||
"elasticsearch",
|
||||
"elasticsearch_schema",
|
||||
"enums",
|
||||
"enums_public",
|
||||
"errors",
|
||||
"fal_client",
|
||||
"ffmpeg_utils",
|
||||
@@ -11288,7 +11263,6 @@ dependencies = [
|
||||
"pager",
|
||||
"password",
|
||||
"primitives",
|
||||
"prompt_engineering",
|
||||
"r2d2",
|
||||
"rand 0.9.2",
|
||||
"redis 1.0.2",
|
||||
@@ -12888,6 +12862,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"subprocess",
|
||||
"tempdir",
|
||||
@@ -12921,6 +12896,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"tempdir",
|
||||
"tokio",
|
||||
@@ -13377,7 +13353,6 @@ dependencies = [
|
||||
"bucket_paths",
|
||||
"cloud_storage",
|
||||
"concurrency",
|
||||
"config",
|
||||
"easyenv",
|
||||
"errors",
|
||||
"ffmpeg_utils",
|
||||
@@ -13459,6 +13434,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"subprocess",
|
||||
"tempdir",
|
||||
@@ -13489,6 +13465,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"subprocess",
|
||||
"tempdir",
|
||||
@@ -14760,7 +14737,6 @@ dependencies = [
|
||||
"bucket_paths",
|
||||
"cloud_storage",
|
||||
"concurrency",
|
||||
"config",
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
@@ -14773,6 +14749,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"server_environment",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"tokens",
|
||||
"tokio",
|
||||
|
||||
@@ -94,7 +94,6 @@ members = [
|
||||
"crates/lib/deprecated/http_server_common", # TODO(bt, 2023-03-20): Move useful pieces to actix_helpers
|
||||
"crates/lib/deprecated/image_algorithms",
|
||||
"crates/lib/deprecated/machine_learning/ml_weights_registry",
|
||||
"crates/lib/deprecated/machine_learning/prompt_engineering",
|
||||
"crates/lib/deprecated/subprocess_common",
|
||||
"crates/lib/deprecated/tts_common",
|
||||
|
||||
@@ -117,7 +116,6 @@ members = [
|
||||
# Schema (public): common strongly-typed tokens, ids, payloads, etc.
|
||||
"crates/schema/public/composite_identifiers",
|
||||
"crates/schema/public/enums",
|
||||
"crates/schema/public/enums_public",
|
||||
"crates/schema/public/reusable_types", # TODO(bt, 2022-12-21): Deprecate
|
||||
"crates/schema/public/tokens",
|
||||
|
||||
@@ -238,7 +236,6 @@ cookie_store = { path = "crates/lib/cookie_store" }
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
easyenv = { path = "crates/lib/easyenv" }
|
||||
enums = { path = "crates/schema/public/enums" }
|
||||
enums_public = { path = "crates/schema/public/enums_public" }
|
||||
errors = { path = "crates/lib/errors" }
|
||||
fal_client = { path = "crates/api_clients/fal_client" }
|
||||
ffmpeg_utils = { path = "crates/lib/ffmpeg_utils" }
|
||||
|
||||
@@ -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
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
$kind: http-request
|
||||
name: Add Wallet Banked Balance
|
||||
url: "{{api_host}}/v1/moderation/wallet/wallet_2pz2h3m2bdfvna7m3b92ccwwr/add_ba\
|
||||
nked_balance"
|
||||
method: POST
|
||||
headers:
|
||||
- key: Accept
|
||||
value: application/json
|
||||
- key: Session
|
||||
value: ""
|
||||
disabled: true
|
||||
- key: Session
|
||||
value: ""
|
||||
disabled: true
|
||||
body:
|
||||
type: json
|
||||
content: |
|
||||
{
|
||||
"title": "test alert from thread 2",
|
||||
"description": "testing testing test"
|
||||
}
|
||||
order: 3365370184335612
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
$kind: http-request
|
||||
name: Create Wallet for User
|
||||
url: "{{api_host}}/v1/moderation/wallet/create_for_user"
|
||||
method: POST
|
||||
headers:
|
||||
- key: Accept
|
||||
value: application/json
|
||||
- key: Session
|
||||
value: ""
|
||||
disabled: true
|
||||
- key: Session
|
||||
value: ""
|
||||
disabled: true
|
||||
body:
|
||||
type: json
|
||||
content: |
|
||||
{
|
||||
"user_token": "user_jg7ng4kdt2x5y", // newuser946
|
||||
"payments_namespace": "artcraft"
|
||||
}
|
||||
order: 4100053395280358
|
||||
@@ -8,6 +8,6 @@ body:
|
||||
type: json
|
||||
content: |-
|
||||
{
|
||||
"google_credential": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImIyN...[redacted]"
|
||||
"google_credential": "...[redacted]"
|
||||
}
|
||||
order: 4000
|
||||
|
||||
@@ -21,7 +21,6 @@ workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
enums.workspace = true
|
||||
enums_public.workspace = true
|
||||
tokens.workspace = true
|
||||
|
||||
# External
|
||||
|
||||
@@ -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.
|
||||
|
||||
+2
-2
@@ -89,7 +89,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_serialization_text_to_image() {
|
||||
let request = EstimateImageCostRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: GenerationProvider::Artcraft,
|
||||
generation_mode: GenerationMode::TextToImage,
|
||||
aspect_ratio: None,
|
||||
@@ -103,7 +103,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_serialization_image_edit() {
|
||||
let request = EstimateImageCostRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: GenerationProvider::Artcraft,
|
||||
generation_mode: GenerationMode::ImageEdit { count: 2 },
|
||||
aspect_ratio: None,
|
||||
|
||||
+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>,
|
||||
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ pub struct OmniGenImageModelDetails {
|
||||
impl Default for OmniGenImageModelDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
full_name: None,
|
||||
text_prompt_supported: None,
|
||||
text_prompt_max_length: None,
|
||||
|
||||
+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)]
|
||||
|
||||
@@ -26,7 +26,6 @@ workspace-build-acceleration.workspace = true
|
||||
artcraft_api_defs.workspace = true
|
||||
cloudflare_errors.workspace = true
|
||||
enums.workspace = true
|
||||
enums_public.workspace = true
|
||||
errors.workspace = true
|
||||
uuid_utils.workspace = true
|
||||
mimetypes.workspace = true
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::by_table::model_weights::weights_category::WeightsCategory;
|
||||
use enums::common::visibility::Visibility;
|
||||
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use enums::by_table::model_weights::weights_types::WeightsType;
|
||||
use log::debug;
|
||||
use serde_derive::Deserialize;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
@@ -193,7 +193,7 @@ pub struct GetMediaFileModeratorFields {
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct GetMediaFileModelInfo {
|
||||
pub weight_token: ModelWeightToken,
|
||||
pub weight_type: PublicWeightsType,
|
||||
pub weight_type: WeightsType,
|
||||
pub weight_category: WeightsCategory,
|
||||
pub title: String,
|
||||
|
||||
|
||||
-1
@@ -8,7 +8,6 @@ use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::http_get_anonymous::http_get_anonymous;
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
|
||||
-1
@@ -7,7 +7,6 @@ use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::http_get_anonymous::http_get_anonymous;
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
|
||||
-1
@@ -8,7 +8,6 @@ use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::http_get_anonymous::http_get_anonymous;
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
|
||||
-1
@@ -5,7 +5,6 @@ use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::http_get_anonymous::http_get_anonymous;
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
|
||||
-1
@@ -5,7 +5,6 @@ use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::http_get_anonymous::http_get_anonymous;
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum AudioListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
/// Forward-compatible character reference list.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum CharacterListRef<'a> {
|
||||
CharacterTokens(&'a Vec<CharacterToken>),
|
||||
|
||||
// In the future, we may have other identifiers for characters.
|
||||
}
|
||||
@@ -16,11 +16,11 @@ pub enum CommonImageModel {
|
||||
#[serde(rename = "gpt_image_1p5")]
|
||||
GptImage1p5,
|
||||
#[serde(rename = "nano_banana")]
|
||||
NanaBanana,
|
||||
NanoBanana,
|
||||
#[serde(rename = "nano_banana_2")]
|
||||
NanaBanana2,
|
||||
NanoBanana2,
|
||||
#[serde(rename = "nano_banana_pro")]
|
||||
NanaBananaPro,
|
||||
NanoBananaPro,
|
||||
#[serde(rename = "seedream_4")]
|
||||
Seedream4,
|
||||
#[serde(rename = "seedream_4p5")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ImageListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ImageRef<'a> {
|
||||
MediaFileToken(&'a MediaFileToken),
|
||||
Url(&'a str),
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum VideoListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ mod tests {
|
||||
|
||||
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBanana,
|
||||
model: CommonImageModel::NanoBanana,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ mod tests {
|
||||
|
||||
fn estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBanana2,
|
||||
model: CommonImageModel::NanoBanana2,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ mod tests {
|
||||
|
||||
fn estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::PlanFalFlux1Dev;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_1_dev(
|
||||
plan: &PlanFalFlux1Dev<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.025 per megapixel, billed by rounding up to nearest megapixel.
|
||||
// Default image sizes are ~1MP (1024x1024), so ~$0.025/image = 3 cents rounded up.
|
||||
//
|
||||
// Fal docs:
|
||||
// Your request will cost $0.025 per megapixel, with images billed by rounding up
|
||||
// to the nearest megapixel.
|
||||
//
|
||||
let cost_per_image: u64 = 3;
|
||||
let num_images = plan.num_images.as_u64();
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::PlanFalFlux1Schnell;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_1_schnell(
|
||||
plan: &PlanFalFlux1Schnell<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.003 per megapixel, billed by rounding up to nearest megapixel.
|
||||
// Default image sizes are ~1MP (1024x1024), so ~$0.003/image = 1 cent rounded up.
|
||||
//
|
||||
// Fal docs:
|
||||
// Your request will cost $0.003 per megapixel, with images billed by rounding up
|
||||
// to the nearest megapixel.
|
||||
//
|
||||
let cost_per_image: u64 = 1;
|
||||
let num_images = plan.num_images.as_u64();
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::{
|
||||
FalNbNumImages, PlanFalNanoBanana,
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_nano_banana(
|
||||
plan: &PlanFalNanoBanana<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.039/image (no resolution multiplier — Gemini 25 Flash has no resolution option).
|
||||
// Rounded to 4 cents per image.
|
||||
//
|
||||
// Fal docs:
|
||||
// Your request will cost $0.039 per image. For $1.00, you can run this model 25 times.
|
||||
//
|
||||
let cost_per_image: u64 = 4;
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
FalNbNumImages::One => 1,
|
||||
FalNbNumImages::Two => 2,
|
||||
FalNbNumImages::Three => 3,
|
||||
FalNbNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::{
|
||||
FalNb2NumImages, FalNb2Resolution, PlanFalNanaBanana2,
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_nano_banana_2(
|
||||
plan: &PlanFalNanaBanana2<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.08/image base (1K). Multipliers: 0.5K = 0.75x, 2K = 1.5x, 4K = 2x.
|
||||
// (Stored as USD cents.)
|
||||
//
|
||||
// Fal docs:
|
||||
// Your request will cost $0.08 per image. For $1.00, you can run this model 12 times.
|
||||
// 2K and 4K outputs will be charged at 1.5 times and 2 times the standard rate, respectively.
|
||||
// 0.5K (512px) resolution outputs will be charged at 0.75 times the standard rate. If web
|
||||
// search is used, an additional $0.015 will be charged. If high thinking is used,
|
||||
// an additional $0.002 will be charged. Note: Pricing is subject to change.
|
||||
//
|
||||
let cost_per_image: u64 = match plan.resolution {
|
||||
Some(FalNb2Resolution::HalfK) => 6, // $0.06 = $0.08 * 0.75
|
||||
Some(FalNb2Resolution::OneK) => 8, // $0.08 base
|
||||
Some(FalNb2Resolution::TwoK) => 12, // $0.12 = $0.08 * 1.5
|
||||
Some(FalNb2Resolution::FourK) => 16, // $0.16 = $0.08 * 2.0
|
||||
None => 8, // default = 1K base
|
||||
};
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
FalNb2NumImages::One => 1,
|
||||
FalNb2NumImages::Two => 2,
|
||||
FalNb2NumImages::Three => 3,
|
||||
FalNb2NumImages::Four => 4,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -42,7 +42,7 @@ mod tests {
|
||||
|
||||
fn estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: Provider::Fal,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
|
||||
+4
@@ -1 +1,5 @@
|
||||
pub(crate) mod estimate_image_cost_fal_flux_1_dev;
|
||||
pub(crate) mod estimate_image_cost_fal_flux_1_schnell;
|
||||
pub(crate) mod estimate_image_cost_fal_nano_banana;
|
||||
pub(crate) mod estimate_image_cost_fal_nano_banana_2;
|
||||
pub(crate) mod estimate_image_cost_fal_nano_banana_pro;
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::PlanFalFlux1Dev;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_flux_1_dev_edit_image_webhook::{
|
||||
enqueue_flux_1_dev_edit_image_webhook, Flux1DevEditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_1_dev_text_to_image_webhook::{
|
||||
enqueue_flux_1_dev_text_to_image_webhook, Flux1DevArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_flux_1_dev(
|
||||
plan: &PlanFalFlux1Dev<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if let Some(image_url) = &plan.maybe_image_url {
|
||||
// Image-to-image mode
|
||||
let args = Flux1DevEditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_url: image_url.clone(),
|
||||
num_images: plan.num_images.to_edit(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_flux_1_dev_edit_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
// Text-to-image mode
|
||||
let args = Flux1DevArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
num_images: plan.num_images.to_t2i(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_flux_1_dev_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::PlanFalFlux1Schnell;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_flux_1_schnell_edit_image_webhook::{
|
||||
enqueue_flux_1_schnell_edit_image_webhook, Flux1SchnellEditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_1_schnell_text_to_image_webhook::{
|
||||
enqueue_flux_1_schnell_text_to_image_webhook, Flux1SchnellArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_flux_1_schnell(
|
||||
plan: &PlanFalFlux1Schnell<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if let Some(image_url) = &plan.maybe_image_url {
|
||||
// Image-to-image (redux) mode
|
||||
let args = Flux1SchnellEditImageArgs {
|
||||
image_url: image_url.clone(),
|
||||
num_images: plan.num_images.to_edit(),
|
||||
image_size: plan.edit_image_size,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_flux_1_schnell_edit_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
// Text-to-image mode
|
||||
let args = Flux1SchnellArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
aspect_ratio: plan.t2i_aspect_ratio,
|
||||
num_images: plan.num_images.to_t2i(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_flux_1_schnell_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::{
|
||||
FalNbNumImages, PlanFalNanoBanana,
|
||||
};
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{
|
||||
enqueue_gemini_25_flash_edit_webhook, Gemini25FlashEditArgs,
|
||||
Gemini25FlashEditNumImages,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::{
|
||||
enqueue_gemini_25_flash_text_to_image_webhook, Gemini25FlashTextToImageArgs,
|
||||
Gemini25FlashTextToImageNumImages,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_nano_banana(
|
||||
plan: &PlanFalNanoBanana<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
// Text-to-image mode
|
||||
let args = Gemini25FlashTextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
num_images: to_t2i_num_images(plan.num_images),
|
||||
aspect_ratio: plan.t2i_aspect_ratio,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_gemini_25_flash_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
// Image-edit mode
|
||||
let args = Gemini25FlashEditArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: to_edit_num_images(plan.num_images),
|
||||
aspect_ratio: plan.edit_aspect_ratio,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_gemini_25_flash_edit_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_t2i_num_images(n: FalNbNumImages) -> Gemini25FlashTextToImageNumImages {
|
||||
match n {
|
||||
FalNbNumImages::One => Gemini25FlashTextToImageNumImages::One,
|
||||
FalNbNumImages::Two => Gemini25FlashTextToImageNumImages::Two,
|
||||
FalNbNumImages::Three => Gemini25FlashTextToImageNumImages::Three,
|
||||
FalNbNumImages::Four => Gemini25FlashTextToImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_num_images(n: FalNbNumImages) -> Gemini25FlashEditNumImages {
|
||||
match n {
|
||||
FalNbNumImages::One => Gemini25FlashEditNumImages::One,
|
||||
FalNbNumImages::Two => Gemini25FlashEditNumImages::Two,
|
||||
FalNbNumImages::Three => Gemini25FlashEditNumImages::Three,
|
||||
FalNbNumImages::Four => Gemini25FlashEditNumImages::Four,
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::{
|
||||
FalNb2NumImages, FalNb2Resolution, PlanFalNanaBanana2,
|
||||
};
|
||||
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_2_edit_image_webhook::{
|
||||
enqueue_nano_banana_2_edit_image_webhook, EnqueueNanoBanana2EditImageArgs,
|
||||
EnqueueNanoBanana2EditImageNumImages, EnqueueNanoBanana2EditImageResolution,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_nano_banana_2_text_to_image_webhook::{
|
||||
enqueue_nano_banana_2_text_to_image_webhook, EnqueueNanoBanana2TextToImageArgs,
|
||||
EnqueueNanoBanana2TextToImageNumImages, EnqueueNanoBanana2TextToImageResolution,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_nano_banana_2(
|
||||
plan: &PlanFalNanaBanana2<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
// Text-to-image mode
|
||||
let args = EnqueueNanoBanana2TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
num_images: to_t2i_num_images(plan.num_images),
|
||||
resolution: plan.resolution.map(to_t2i_resolution),
|
||||
aspect_ratio: plan.t2i_aspect_ratio,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_nano_banana_2_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
// Image-edit mode
|
||||
let args = EnqueueNanoBanana2EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: to_edit_num_images(plan.num_images),
|
||||
resolution: plan.resolution.map(to_edit_resolution),
|
||||
aspect_ratio: plan.edit_aspect_ratio,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_nano_banana_2_edit_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_t2i_num_images(n: FalNb2NumImages) -> EnqueueNanoBanana2TextToImageNumImages {
|
||||
match n {
|
||||
FalNb2NumImages::One => EnqueueNanoBanana2TextToImageNumImages::One,
|
||||
FalNb2NumImages::Two => EnqueueNanoBanana2TextToImageNumImages::Two,
|
||||
FalNb2NumImages::Three => EnqueueNanoBanana2TextToImageNumImages::Three,
|
||||
FalNb2NumImages::Four => EnqueueNanoBanana2TextToImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_num_images(n: FalNb2NumImages) -> EnqueueNanoBanana2EditImageNumImages {
|
||||
match n {
|
||||
FalNb2NumImages::One => EnqueueNanoBanana2EditImageNumImages::One,
|
||||
FalNb2NumImages::Two => EnqueueNanoBanana2EditImageNumImages::Two,
|
||||
FalNb2NumImages::Three => EnqueueNanoBanana2EditImageNumImages::Three,
|
||||
FalNb2NumImages::Four => EnqueueNanoBanana2EditImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_resolution(r: FalNb2Resolution) -> EnqueueNanoBanana2TextToImageResolution {
|
||||
match r {
|
||||
FalNb2Resolution::HalfK => EnqueueNanoBanana2TextToImageResolution::HalfK,
|
||||
FalNb2Resolution::OneK => EnqueueNanoBanana2TextToImageResolution::OneK,
|
||||
FalNb2Resolution::TwoK => EnqueueNanoBanana2TextToImageResolution::TwoK,
|
||||
FalNb2Resolution::FourK => EnqueueNanoBanana2TextToImageResolution::FourK,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_resolution(r: FalNb2Resolution) -> EnqueueNanoBanana2EditImageResolution {
|
||||
match r {
|
||||
FalNb2Resolution::HalfK => EnqueueNanoBanana2EditImageResolution::HalfK,
|
||||
FalNb2Resolution::OneK => EnqueueNanoBanana2EditImageResolution::OneK,
|
||||
FalNb2Resolution::TwoK => EnqueueNanoBanana2EditImageResolution::TwoK,
|
||||
FalNb2Resolution::FourK => EnqueueNanoBanana2EditImageResolution::FourK,
|
||||
}
|
||||
}
|
||||
+4
@@ -1 +1,5 @@
|
||||
pub mod generate_image_fal_flux_1_dev;
|
||||
pub mod generate_image_fal_flux_1_schnell;
|
||||
pub mod generate_image_fal_nano_banana;
|
||||
pub mod generate_image_fal_nano_banana_2;
|
||||
pub mod generate_image_fal_nano_banana_pro;
|
||||
|
||||
+15
-6
@@ -10,19 +10,24 @@ use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::plan_generate_image_artcraft_flux_1_dev;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_schnell::plan_generate_image_artcraft_flux_1_schnell;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::plan_generate_image_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1::plan_generate_image_artcraft_flux_pro_1p1;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::plan_generate_image_artcraft_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::plan_generate_image_artcraft_gpt_image_1p5;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::plan_generate_image_artcraft_nano_banana;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::plan_generate_image_artcraft_nano_banana_2;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::plan_generate_image_artcraft_nano_banana_pro;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::plan_generate_image_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::plan_generate_image_artcraft_seedream_4;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::plan_generate_image_artcraft_seedream_4p5;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::plan_generate_image_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::plan_generate_image_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::plan_generate_image_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::plan_generate_image_fal_flux_1_dev;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::plan_generate_image_fal_flux_1_schnell;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::plan_generate_image_fal_nano_banana;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::plan_generate_image_fal_nano_banana_2;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::plan_generate_image_fal_nano_banana_pro;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GenerateImageRequest<'a> {
|
||||
/// Which model to use.
|
||||
pub model: CommonImageModel,
|
||||
@@ -86,9 +91,9 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
CommonImageModel::FluxPro11 => plan_generate_image_artcraft_flux_pro_1p1(self),
|
||||
CommonImageModel::FluxPro11Ultra => plan_generate_image_artcraft_flux_pro_1p1_ultra(self),
|
||||
CommonImageModel::GptImage1p5 => plan_generate_image_artcraft_gpt_image_1p5(self),
|
||||
CommonImageModel::NanaBanana => plan_generate_image_artcraft_nano_banana(self),
|
||||
CommonImageModel::NanaBanana2 => plan_generate_image_artcraft_nano_banana_2(self),
|
||||
CommonImageModel::NanaBananaPro => plan_generate_image_artcraft_nano_banana_pro(self),
|
||||
CommonImageModel::NanoBanana => plan_generate_image_artcraft_nano_banana(self),
|
||||
CommonImageModel::NanoBanana2 => plan_generate_image_artcraft_nano_banana_2(self),
|
||||
CommonImageModel::NanoBananaPro => plan_generate_image_artcraft_nano_banana_pro(self),
|
||||
CommonImageModel::Seedream4 => plan_generate_image_artcraft_seedream_4(self),
|
||||
CommonImageModel::Seedream4p5 => plan_generate_image_artcraft_seedream_4p5(self),
|
||||
CommonImageModel::Seedream5Lite => plan_generate_image_artcraft_seedream_5_lite(self),
|
||||
@@ -99,7 +104,11 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
|
||||
fn build_fal(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.model {
|
||||
CommonImageModel::NanaBananaPro => plan_generate_image_fal_nano_banana_pro(self),
|
||||
CommonImageModel::Flux1Dev => plan_generate_image_fal_flux_1_dev(self),
|
||||
CommonImageModel::Flux1Schnell => plan_generate_image_fal_flux_1_schnell(self),
|
||||
CommonImageModel::NanoBanana => plan_generate_image_fal_nano_banana(self),
|
||||
CommonImageModel::NanoBanana2 => plan_generate_image_fal_nano_banana_2(self),
|
||||
CommonImageModel::NanoBananaPro => plan_generate_image_fal_nano_banana_pro(self),
|
||||
_ => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "provider",
|
||||
|
||||
+44
@@ -13,6 +13,10 @@ use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraf
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_5_lite::estimate_image_cost_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_qwen_edit_2511_angles::estimate_image_cost_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_2_lora_angles::estimate_image_cost_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_1_dev::estimate_image_cost_fal_flux_1_dev;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_1_schnell::estimate_image_cost_fal_flux_1_schnell;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana::estimate_image_cost_fal_nano_banana;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_2::estimate_image_cost_fal_nano_banana_2;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_pro::estimate_image_cost_fal_nano_banana_pro;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_dev::execute_artcraft_flux_1_dev;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_schnell::execute_artcraft_flux_1_schnell;
|
||||
@@ -27,6 +31,10 @@ use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_5_lite::execute_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_qwen_edit_2511_angles::execute_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_2_lora_angles::execute_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_1_dev::execute_fal_flux_1_dev;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_1_schnell::execute_fal_flux_1_schnell;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana::execute_fal_nano_banana;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_2::execute_fal_nano_banana_2;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_pro::execute_fal_nano_banana_pro;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
@@ -43,6 +51,10 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::PlanFalFlux1Dev;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::PlanFalFlux1Schnell;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::PlanFalNanoBanana;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::PlanFalNanaBanana2;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::PlanFalNanaBananaPro;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -60,6 +72,10 @@ pub enum ImageGenerationPlan<'a> {
|
||||
ArtcraftSeedream5Lite(PlanArtcraftSeedream5Lite<'a>),
|
||||
ArtcraftQwenEdit2511Angles(PlanArtcraftQwenEdit2511Angles<'a>),
|
||||
ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles<'a>),
|
||||
FalFlux1Dev(PlanFalFlux1Dev<'a>),
|
||||
FalFlux1Schnell(PlanFalFlux1Schnell<'a>),
|
||||
FalNanoBanana(PlanFalNanoBanana<'a>),
|
||||
FalNanaBanana2(PlanFalNanaBanana2<'a>),
|
||||
FalNanaBananaPro(PlanFalNanaBananaPro<'a>),
|
||||
}
|
||||
|
||||
@@ -121,6 +137,22 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_flux_2_lora_angles(plan, artcraft_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalFlux1Dev(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_flux_1_dev(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalFlux1Schnell(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_flux_1_schnell(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalNanoBanana(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_nano_banana(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalNanaBanana2(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_nano_banana_2(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalNanaBananaPro(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_nano_banana_pro(plan, fal_client).await
|
||||
@@ -169,6 +201,18 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) => {
|
||||
estimate_image_cost_artcraft_flux_2_lora_angles(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalFlux1Dev(plan) => {
|
||||
estimate_image_cost_fal_flux_1_dev(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalFlux1Schnell(plan) => {
|
||||
estimate_image_cost_fal_flux_1_schnell(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalNanoBanana(plan) => {
|
||||
estimate_image_cost_fal_nano_banana(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalNanaBanana2(plan) => {
|
||||
estimate_image_cost_fal_nano_banana_2(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalNanaBananaPro(plan) => {
|
||||
estimate_image_cost_fal_nano_banana_pro(plan)
|
||||
}
|
||||
|
||||
+4
@@ -1 +1,5 @@
|
||||
pub mod plan_generate_image_fal_flux_1_dev;
|
||||
pub mod plan_generate_image_fal_flux_1_schnell;
|
||||
pub mod plan_generate_image_fal_nano_banana;
|
||||
pub mod plan_generate_image_fal_nano_banana_2;
|
||||
pub mod plan_generate_image_fal_nano_banana_pro;
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_flux_1_dev_edit_image_webhook::Flux1DevEditImageNumImages;
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_1_dev_text_to_image_webhook::{
|
||||
Flux1DevAspectRatio, Flux1DevNumImages,
|
||||
};
|
||||
|
||||
/// Shared num-images type for both t2i and edit modes.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalFlux1DevNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalFlux1Dev<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Image URL for editing. None = text-to-image mode.
|
||||
pub maybe_image_url: Option<String>,
|
||||
pub aspect_ratio: Flux1DevAspectRatio,
|
||||
pub num_images: FalFlux1DevNumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_flux_1_dev<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let maybe_image_url = resolve_single_image_url(request.image_inputs)?;
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalFlux1Dev(PlanFalFlux1Dev {
|
||||
prompt: request.prompt,
|
||||
maybe_image_url,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_single_image_url(
|
||||
image_inputs: Option<ImageListRef<'_>>,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::Urls(urls)) => {
|
||||
if urls.is_empty() {
|
||||
Ok(None)
|
||||
} else if urls.len() == 1 {
|
||||
Ok(Some(urls[0].clone()))
|
||||
} else {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Flux 1 Dev image-to-image supports exactly 1 image, got {}", urls.len()),
|
||||
}))
|
||||
}
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Flux1DevAspectRatio, ArtcraftRouterError> {
|
||||
use Flux1DevAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Ar::SquareHd),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::PortraitNineBySixteen),
|
||||
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Ar::PortraitNineBySixteen),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Ar::Square),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalFlux1DevNumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalFlux1DevNumImages::One),
|
||||
2 => Ok(FalFlux1DevNumImages::Two),
|
||||
3 => Ok(FalFlux1DevNumImages::Three),
|
||||
4 => Ok(FalFlux1DevNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalFlux1DevNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalFlux1DevNumImages {
|
||||
pub fn to_t2i(self) -> Flux1DevNumImages {
|
||||
match self {
|
||||
Self::One => Flux1DevNumImages::One,
|
||||
Self::Two => Flux1DevNumImages::Two,
|
||||
Self::Three => Flux1DevNumImages::Three,
|
||||
Self::Four => Flux1DevNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> Flux1DevEditImageNumImages {
|
||||
match self {
|
||||
Self::One => Flux1DevEditImageNumImages::One,
|
||||
Self::Two => Flux1DevEditImageNumImages::Two,
|
||||
Self::Three => Flux1DevEditImageNumImages::Three,
|
||||
Self::Four => Flux1DevEditImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_flux_1_schnell_edit_image_webhook::{
|
||||
Flux1SchnellEditImageNumImages, Flux1SchnellEditImageSize,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_1_schnell_text_to_image_webhook::{
|
||||
Flux1SchnellAspectRatio, Flux1SchnellNumImages,
|
||||
};
|
||||
|
||||
/// Shared num-images type for both t2i and edit modes.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalFlux1SchnellNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalFlux1Schnell<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Image URL for editing. None = text-to-image mode.
|
||||
pub maybe_image_url: Option<String>,
|
||||
pub t2i_aspect_ratio: Flux1SchnellAspectRatio,
|
||||
pub edit_image_size: Option<Flux1SchnellEditImageSize>,
|
||||
pub num_images: FalFlux1SchnellNumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_flux_1_schnell<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let maybe_image_url = resolve_single_image_url(request.image_inputs)?;
|
||||
let t2i_aspect_ratio = plan_t2i_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let edit_image_size = plan_edit_image_size(request.aspect_ratio);
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalFlux1Schnell(PlanFalFlux1Schnell {
|
||||
prompt: request.prompt,
|
||||
maybe_image_url,
|
||||
t2i_aspect_ratio,
|
||||
edit_image_size,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_single_image_url(
|
||||
image_inputs: Option<ImageListRef<'_>>,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::Urls(urls)) => {
|
||||
if urls.is_empty() {
|
||||
Ok(None)
|
||||
} else if urls.len() == 1 {
|
||||
Ok(Some(urls[0].clone()))
|
||||
} else {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Flux 1 Schnell redux supports exactly 1 image, got {}", urls.len()),
|
||||
}))
|
||||
}
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_t2i_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Flux1SchnellAspectRatio, ArtcraftRouterError> {
|
||||
use Flux1SchnellAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto4k) => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::Square) => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Ar::SquareHd),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::PortraitNineBySixteen),
|
||||
Some(CommonAspectRatio::WideFiveByFour) | Some(CommonAspectRatio::WideThreeByTwo) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallFourByFive) | Some(CommonAspectRatio::TallTwoByThree) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Ar::PortraitNineBySixteen),
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Ar::Square),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_edit_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<Flux1SchnellEditImageSize> {
|
||||
use Flux1SchnellEditImageSize as Size;
|
||||
match aspect_ratio {
|
||||
None => None,
|
||||
Some(CommonAspectRatio::Square) => Some(Size::Square),
|
||||
Some(CommonAspectRatio::SquareHd) => Some(Size::SquareHd),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Some(Size::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Some(Size::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Some(Size::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Some(Size::PortraitNineBySixteen),
|
||||
_ => Some(Size::Square),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalFlux1SchnellNumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalFlux1SchnellNumImages::One),
|
||||
2 => Ok(FalFlux1SchnellNumImages::Two),
|
||||
3 => Ok(FalFlux1SchnellNumImages::Three),
|
||||
4 => Ok(FalFlux1SchnellNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalFlux1SchnellNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalFlux1SchnellNumImages {
|
||||
pub fn to_t2i(self) -> Flux1SchnellNumImages {
|
||||
match self {
|
||||
Self::One => Flux1SchnellNumImages::One,
|
||||
Self::Two => Flux1SchnellNumImages::Two,
|
||||
Self::Three => Flux1SchnellNumImages::Three,
|
||||
Self::Four => Flux1SchnellNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> Flux1SchnellEditImageNumImages {
|
||||
match self {
|
||||
Self::One => Flux1SchnellEditImageNumImages::One,
|
||||
Self::Two => Flux1SchnellEditImageNumImages::Two,
|
||||
Self::Three => Flux1SchnellEditImageNumImages::Three,
|
||||
Self::Four => Flux1SchnellEditImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::Gemini25FlashEditAspectRatio;
|
||||
use fal_client::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::Gemini25FlashTextToImageAspectRatio;
|
||||
|
||||
/// Intermediate num-images type shared between t2i and edit enums.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalNbNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalNanoBanana<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Image URLs for editing. Empty vec = text-to-image mode.
|
||||
pub image_urls: Vec<String>,
|
||||
/// Pre-resolved aspect ratio for text-to-image mode.
|
||||
pub t2i_aspect_ratio: Option<Gemini25FlashTextToImageAspectRatio>,
|
||||
/// Pre-resolved aspect ratio for image-edit mode.
|
||||
pub edit_aspect_ratio: Option<Gemini25FlashEditAspectRatio>,
|
||||
/// Pre-resolved number of images (1–4, shared for both modes).
|
||||
pub num_images: FalNbNumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_nano_banana<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
let image_urls = resolve_image_list_ref(request.image_inputs)?;
|
||||
let t2i_aspect_ratio = plan_t2i_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let edit_aspect_ratio = plan_edit_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalNanoBanana(PlanFalNanoBanana {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
t2i_aspect_ratio,
|
||||
edit_aspect_ratio,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef<'_>>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gemini 25 Flash text-to-image supported aspect ratios:
|
||||
// 1:1, 5:4, 4:3, 3:2, 16:9, 21:9, 4:5, 3:4, 2:3, 9:16
|
||||
// (no Auto for text-to-image)
|
||||
fn plan_t2i_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Gemini25FlashTextToImageAspectRatio>, ArtcraftRouterError> {
|
||||
use Gemini25FlashTextToImageAspectRatio as T2iAr;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
// Auto not valid for text-to-image; fall back to square
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Some(T2iAr::OneByOne)),
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(T2iAr::OneByOne)),
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(T2iAr::FiveByFour)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(T2iAr::FourByThree)),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(T2iAr::ThreeByTwo)),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(T2iAr::SixteenByNine)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(T2iAr::TwentyOneByNine)),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(T2iAr::FourByFive)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(T2iAr::ThreeByFour)),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(T2iAr::TwoByThree)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(T2iAr::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(T2iAr::NineBySixteen))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Gemini 25 Flash image-edit supported aspect ratios: same as t2i plus Auto
|
||||
fn plan_edit_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
is_edit_mode: bool,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Gemini25FlashEditAspectRatio>, ArtcraftRouterError> {
|
||||
use Gemini25FlashEditAspectRatio as EditAr;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => {
|
||||
if is_edit_mode {
|
||||
Ok(Some(EditAr::Auto))
|
||||
} else {
|
||||
Ok(Some(EditAr::OneByOne))
|
||||
}
|
||||
}
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(EditAr::OneByOne)),
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(EditAr::FiveByFour)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(EditAr::FourByThree)),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(EditAr::ThreeByTwo)),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(EditAr::SixteenByNine)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(EditAr::TwentyOneByNine)),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(EditAr::FourByFive)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(EditAr::ThreeByFour)),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(EditAr::TwoByThree)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(EditAr::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(EditAr::NineBySixteen))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalNbNumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalNbNumImages::One),
|
||||
2 => Ok(FalNbNumImages::Two),
|
||||
3 => Ok(FalNbNumImages::Three),
|
||||
4 => Ok(FalNbNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(FalNbNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_2_edit_image_webhook::EnqueueNanoBanana2EditImageAspectRatio;
|
||||
use fal_client::requests::webhook::image::text::enqueue_nano_banana_2_text_to_image_webhook::EnqueueNanoBanana2TextToImageAspectRatio;
|
||||
|
||||
/// Intermediate resolution type shared between t2i and edit enums.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalNb2Resolution {
|
||||
HalfK,
|
||||
OneK,
|
||||
TwoK,
|
||||
FourK,
|
||||
}
|
||||
|
||||
/// Intermediate num-images type shared between t2i and edit enums.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalNb2NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalNanaBanana2<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Image URLs for editing. Empty vec = text-to-image mode.
|
||||
pub image_urls: Vec<String>,
|
||||
/// Pre-resolved aspect ratio for text-to-image mode.
|
||||
pub t2i_aspect_ratio: Option<EnqueueNanoBanana2TextToImageAspectRatio>,
|
||||
/// Pre-resolved aspect ratio for image-edit mode.
|
||||
pub edit_aspect_ratio: Option<EnqueueNanoBanana2EditImageAspectRatio>,
|
||||
/// Pre-resolved resolution (shared for both modes).
|
||||
pub resolution: Option<FalNb2Resolution>,
|
||||
/// Pre-resolved number of images (1–4, shared for both modes).
|
||||
pub num_images: FalNb2NumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_nano_banana_2<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
let image_urls = resolve_image_list_ref(request.image_inputs)?;
|
||||
let t2i_aspect_ratio = plan_t2i_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let edit_aspect_ratio = plan_edit_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let resolution = plan_resolution(request.resolution)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalNanaBanana2(PlanFalNanaBanana2 {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
t2i_aspect_ratio,
|
||||
edit_aspect_ratio,
|
||||
resolution,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef<'_>>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NB2 text-to-image supported aspect ratios:
|
||||
// auto, 1:1, 5:4, 4:3, 3:2, 16:9, 21:9, 4:5, 3:4, 2:3, 9:16
|
||||
fn plan_t2i_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<EnqueueNanoBanana2TextToImageAspectRatio>, ArtcraftRouterError> {
|
||||
use EnqueueNanoBanana2TextToImageAspectRatio as T2iAr;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Some(T2iAr::Auto)),
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(T2iAr::OneByOne)),
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(T2iAr::FiveByFour)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(T2iAr::FourByThree)),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(T2iAr::ThreeByTwo)),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(T2iAr::SixteenByNine)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(T2iAr::TwentyOneByNine)),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(T2iAr::FourByFive)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(T2iAr::ThreeByFour)),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(T2iAr::TwoByThree)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(T2iAr::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(T2iAr::NineBySixteen))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NB2 image-edit supported aspect ratios: same as t2i plus Auto
|
||||
fn plan_edit_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
is_edit_mode: bool,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<EnqueueNanoBanana2EditImageAspectRatio>, ArtcraftRouterError> {
|
||||
use EnqueueNanoBanana2EditImageAspectRatio as EditAr;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => {
|
||||
if is_edit_mode {
|
||||
Ok(Some(EditAr::Auto))
|
||||
} else {
|
||||
Ok(Some(EditAr::OneByOne))
|
||||
}
|
||||
}
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(EditAr::OneByOne)),
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(EditAr::FiveByFour)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(EditAr::FourByThree)),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(EditAr::ThreeByTwo)),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(EditAr::SixteenByNine)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(EditAr::TwentyOneByNine)),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(EditAr::FourByFive)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(EditAr::ThreeByFour)),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(EditAr::TwoByThree)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(EditAr::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(EditAr::NineBySixteen))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
) -> Result<Option<FalNb2Resolution>, ArtcraftRouterError> {
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
Some(CommonResolution::HalfK) => Ok(Some(FalNb2Resolution::HalfK)),
|
||||
Some(CommonResolution::OneK) => Ok(Some(FalNb2Resolution::OneK)),
|
||||
Some(CommonResolution::TwoK) => Ok(Some(FalNb2Resolution::TwoK)),
|
||||
Some(CommonResolution::FourK) => Ok(Some(FalNb2Resolution::FourK)),
|
||||
Some(CommonResolution::ThreeK) => Ok(Some(FalNb2Resolution::TwoK)),
|
||||
Some(CommonResolution::FourEightyP) => Ok(Some(FalNb2Resolution::HalfK)),
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(Some(FalNb2Resolution::OneK)),
|
||||
Some(CommonResolution::TenEightyP) => Ok(Some(FalNb2Resolution::OneK)),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalNb2NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalNb2NumImages::One),
|
||||
2 => Ok(FalNb2NumImages::Two),
|
||||
3 => Ok(FalNb2NumImages::Three),
|
||||
4 => Ok(FalNb2NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(FalNb2NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -250,7 +250,7 @@ mod tests {
|
||||
|
||||
fn base_fal_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space"),
|
||||
image_inputs: None,
|
||||
|
||||
+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,
|
||||
};
|
||||
|
||||
+6
-1
@@ -1,7 +1,8 @@
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
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::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
@@ -19,6 +20,7 @@ use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
/// Plan to either (1) generate a video or (2) determine how much it costs to generate that video.
|
||||
/// This works across multiple providers by shaping a generic "GenerateVideoRequest" into a provider-specific plan.
|
||||
/// That plan can then be used to return a cost estimate for that given provider or return a struct that can be used to send a real generation request.
|
||||
#[derive(Debug)]
|
||||
pub struct GenerateVideoRequest<'a> {
|
||||
/// Which model to use.
|
||||
pub model: CommonVideoModel,
|
||||
@@ -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> {
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn get_artcraft_client() -> RouterClient {
|
||||
|
||||
pub fn base_image_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
image_inputs: None,
|
||||
@@ -49,7 +49,7 @@ pub fn get_fal_client() -> RouterClient {
|
||||
|
||||
pub fn base_fal_image_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space"),
|
||||
image_inputs: None,
|
||||
@@ -121,7 +121,7 @@ pub fn base_seedream_5_lite_image_request() -> GenerateImageRequest<'static> {
|
||||
|
||||
pub fn base_nano_banana_2_image_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBanana2,
|
||||
model: CommonImageModel::NanoBanana2,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
image_inputs: None,
|
||||
@@ -139,7 +139,7 @@ pub fn base_nano_banana_2_image_request() -> GenerateImageRequest<'static> {
|
||||
|
||||
pub fn base_nano_banana_image_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBanana,
|
||||
model: CommonImageModel::NanoBanana,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
image_inputs: None,
|
||||
@@ -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,
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
use crate::requests::api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Flux1DevEditImageInput {
|
||||
pub prompt: String,
|
||||
|
||||
pub image_url: String,
|
||||
|
||||
/// 0.0 - 1.0
|
||||
/// Default: 0.95
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub strength: Option<f64>,
|
||||
|
||||
/// 1 - 4
|
||||
/// Default: 1
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_images: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub seed: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub guidance_scale: Option<f64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_inference_steps: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_safety_checker: Option<bool>,
|
||||
|
||||
/// Options: jpeg, png
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sync_mode: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Flux1DevEditImageFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Flux1DevEditImageOutput {
|
||||
pub images: Vec<Flux1DevEditImageFile>,
|
||||
}
|
||||
|
||||
pub fn flux_1_dev_edit_image(
|
||||
params: Flux1DevEditImageInput,
|
||||
) -> FalRequest<Flux1DevEditImageInput, Flux1DevEditImageOutput> {
|
||||
FalRequest::new("fal-ai/flux/dev/image-to-image", params)
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
use crate::requests::api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Flux1SchnellEditImageInput {
|
||||
pub image_url: String,
|
||||
|
||||
/// Options: square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_size: Option<String>,
|
||||
|
||||
/// 1 - 4
|
||||
/// Default: 1
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_images: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub seed: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_inference_steps: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_safety_checker: Option<bool>,
|
||||
|
||||
/// Options: jpeg, png
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sync_mode: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Flux1SchnellEditImageFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Flux1SchnellEditImageOutput {
|
||||
pub images: Vec<Flux1SchnellEditImageFile>,
|
||||
}
|
||||
|
||||
pub fn flux_1_schnell_edit_image(
|
||||
params: Flux1SchnellEditImageInput,
|
||||
) -> FalRequest<Flux1SchnellEditImageInput, Flux1SchnellEditImageOutput> {
|
||||
FalRequest::new("fal-ai/flux/schnell/redux", params)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod http_flux_1_dev_edit_image;
|
||||
pub mod http_flux_1_schnell_edit_image;
|
||||
pub mod http_flux_pro_kontext_max_edit;
|
||||
pub mod http_gemini_25_flash_edit;
|
||||
pub mod http_gpt_image_1_edit_image;
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::http::image::edit::http_flux_1_dev_edit_image::{flux_1_dev_edit_image, Flux1DevEditImageInput};
|
||||
use crate::requests::api::webhook_response::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct Flux1DevEditImageArgs<'a, U: IntoUrl> {
|
||||
pub prompt: &'a str,
|
||||
pub image_url: String,
|
||||
pub num_images: Flux1DevEditImageNumImages,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: U,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Flux1DevEditImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
pub async fn enqueue_flux_1_dev_edit_image_webhook<U: IntoUrl>(
|
||||
args: Flux1DevEditImageArgs<'_, U>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
let num_images = match args.num_images {
|
||||
Flux1DevEditImageNumImages::One => 1,
|
||||
Flux1DevEditImageNumImages::Two => 2,
|
||||
Flux1DevEditImageNumImages::Three => 3,
|
||||
Flux1DevEditImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let request = Flux1DevEditImageInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
image_url: args.image_url,
|
||||
num_images: Some(num_images),
|
||||
enable_safety_checker: Some(false),
|
||||
output_format: Some("png".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = flux_1_dev_edit_image(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::image::edit::enqueue_flux_1_dev_edit_image_webhook::{
|
||||
enqueue_flux_1_dev_edit_image_webhook, Flux1DevEditImageArgs, Flux1DevEditImageNumImages,
|
||||
};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::GHOST_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real Fal API request
|
||||
async fn test_single_image() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Flux1DevEditImageArgs {
|
||||
prompt: "make this image look like a watercolor painting",
|
||||
image_url: GHOST_IMAGE_URL.to_string(),
|
||||
num_images: Flux1DevEditImageNumImages::One,
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_flux_1_dev_edit_image_webhook(args).await?;
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real Fal API request
|
||||
async fn test_batch_two() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Flux1DevEditImageArgs {
|
||||
prompt: "turn this into a cyberpunk scene with neon lights",
|
||||
image_url: GHOST_IMAGE_URL.to_string(),
|
||||
num_images: Flux1DevEditImageNumImages::Two,
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_flux_1_dev_edit_image_webhook(args).await?;
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::http::image::edit::http_flux_1_schnell_edit_image::{flux_1_schnell_edit_image, Flux1SchnellEditImageInput};
|
||||
use crate::requests::api::webhook_response::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct Flux1SchnellEditImageArgs<'a, U: IntoUrl> {
|
||||
pub image_url: String,
|
||||
pub num_images: Flux1SchnellEditImageNumImages,
|
||||
pub image_size: Option<Flux1SchnellEditImageSize>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: U,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Flux1SchnellEditImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Flux1SchnellEditImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
LandscapeFourByThree,
|
||||
LandscapeSixteenByNine,
|
||||
PortraitThreeByFour,
|
||||
PortraitNineBySixteen,
|
||||
}
|
||||
|
||||
pub async fn enqueue_flux_1_schnell_edit_image_webhook<U: IntoUrl>(
|
||||
args: Flux1SchnellEditImageArgs<'_, U>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
let num_images = match args.num_images {
|
||||
Flux1SchnellEditImageNumImages::One => 1,
|
||||
Flux1SchnellEditImageNumImages::Two => 2,
|
||||
Flux1SchnellEditImageNumImages::Three => 3,
|
||||
Flux1SchnellEditImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let image_size = args.image_size.map(|s| match s {
|
||||
Flux1SchnellEditImageSize::Square => "square",
|
||||
Flux1SchnellEditImageSize::SquareHd => "square_hd",
|
||||
Flux1SchnellEditImageSize::LandscapeFourByThree => "landscape_4_3",
|
||||
Flux1SchnellEditImageSize::LandscapeSixteenByNine => "landscape_16_9",
|
||||
Flux1SchnellEditImageSize::PortraitThreeByFour => "portrait_4_3",
|
||||
Flux1SchnellEditImageSize::PortraitNineBySixteen => "portrait_16_9",
|
||||
}.to_string());
|
||||
|
||||
let request = Flux1SchnellEditImageInput {
|
||||
image_url: args.image_url,
|
||||
num_images: Some(num_images),
|
||||
image_size,
|
||||
enable_safety_checker: Some(false),
|
||||
output_format: Some("png".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = flux_1_schnell_edit_image(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::image::edit::enqueue_flux_1_schnell_edit_image_webhook::{
|
||||
enqueue_flux_1_schnell_edit_image_webhook, Flux1SchnellEditImageArgs,
|
||||
Flux1SchnellEditImageNumImages, Flux1SchnellEditImageSize,
|
||||
};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::GHOST_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real Fal API request
|
||||
async fn test_single_image_no_size() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Flux1SchnellEditImageArgs {
|
||||
image_url: GHOST_IMAGE_URL.to_string(),
|
||||
num_images: Flux1SchnellEditImageNumImages::One,
|
||||
image_size: None,
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_flux_1_schnell_edit_image_webhook(args).await?;
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real Fal API request
|
||||
async fn test_with_landscape_size() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Flux1SchnellEditImageArgs {
|
||||
image_url: GHOST_IMAGE_URL.to_string(),
|
||||
num_images: Flux1SchnellEditImageNumImages::Two,
|
||||
image_size: Some(Flux1SchnellEditImageSize::LandscapeSixteenByNine),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_flux_1_schnell_edit_image_webhook(args).await?;
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod enqueue_flux_1_dev_edit_image_webhook;
|
||||
pub mod enqueue_flux_1_schnell_edit_image_webhook;
|
||||
pub mod enqueue_bytedance_seedream_v4_edit_image_webhook;
|
||||
pub mod enqueue_bytedance_seedream_v4p5_edit_image_webhook;
|
||||
pub mod enqueue_bytedance_seedream_v5_lite_edit_image_webhook;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use wreq::StatusCode;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Seedance2ProSpecificApiError {
|
||||
/// The session cookies are expired or invalid.
|
||||
@@ -8,6 +10,12 @@ pub enum Seedance2ProSpecificApiError {
|
||||
|
||||
/// The video generation request was flagged as a content violation.
|
||||
VideoGenerationViolation(String),
|
||||
|
||||
/// The account does not have enough credits to perform the operation.
|
||||
BillingError {
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Error for Seedance2ProSpecificApiError {}
|
||||
@@ -17,6 +25,7 @@ impl Display for Seedance2ProSpecificApiError {
|
||||
match self {
|
||||
Self::UnauthorizedSessionExpired => write!(f, "Unauthorized: session cookies expired or invalid."),
|
||||
Self::VideoGenerationViolation(body) => write!(f, "Video generation violation: {}", body),
|
||||
Self::BillingError { status_code, body } => write!(f, "Billing error (status {}): {}", status_code, body),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+151
-4
@@ -5,6 +5,7 @@ use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
use crate::requests::generate_video::request_types::*;
|
||||
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
|
||||
use crate::utils::categorize_seedance2pro_error::categorize_seedance2pro_error;
|
||||
use crate::utils::common_headers::FIREFOX_USER_AGENT;
|
||||
use log::info;
|
||||
use wreq::Client;
|
||||
@@ -48,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>,
|
||||
|
||||
@@ -68,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()
|
||||
@@ -260,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,
|
||||
@@ -308,10 +315,7 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
|
||||
info!("Response status: {}, body: {}", status, response_body);
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: status,
|
||||
body: response_body,
|
||||
}.into());
|
||||
return Err(categorize_seedance2pro_error(status, response_body));
|
||||
}
|
||||
|
||||
let batch_response: Vec<BatchResponseItem> = serde_json::from_str(&response_body)
|
||||
@@ -373,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,
|
||||
}
|
||||
@@ -497,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,
|
||||
};
|
||||
@@ -526,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,
|
||||
};
|
||||
@@ -557,6 +564,7 @@ mod tests {
|
||||
]),
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
@@ -587,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,
|
||||
};
|
||||
@@ -619,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,
|
||||
};
|
||||
@@ -673,6 +683,7 @@ mod tests {
|
||||
result.public_url,
|
||||
]),
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
@@ -722,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,
|
||||
};
|
||||
@@ -773,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,
|
||||
};
|
||||
@@ -831,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,
|
||||
};
|
||||
@@ -882,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,
|
||||
};
|
||||
@@ -893,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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
use wreq::StatusCode;
|
||||
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
|
||||
/// Known error message substrings that indicate a billing/credits error.
|
||||
const BILLING_ERROR_MARKERS: &[&str] = &[
|
||||
"credits not enough",
|
||||
];
|
||||
|
||||
/// Categorize a non-success HTTP response into a specific or generic error.
|
||||
///
|
||||
/// Checks the response body for known patterns (e.g. billing errors) and returns
|
||||
/// a specific error variant when possible, falling back to
|
||||
/// `UncategorizedBadResponseWithStatusAndBody` otherwise.
|
||||
pub fn categorize_seedance2pro_error(
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
) -> Seedance2ProError {
|
||||
if is_billing_error(&body) {
|
||||
return Seedance2ProSpecificApiError::BillingError {
|
||||
status_code,
|
||||
body,
|
||||
}.into();
|
||||
}
|
||||
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code,
|
||||
body,
|
||||
}.into()
|
||||
}
|
||||
|
||||
/// Check if the response body contains a known billing error marker.
|
||||
fn is_billing_error(body: &str) -> bool {
|
||||
let body_lower = body.to_lowercase();
|
||||
BILLING_ERROR_MARKERS.iter().any(|marker| body_lower.contains(marker))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
|
||||
#[test]
|
||||
fn real_not_enough_credits_response() {
|
||||
let body = std::fs::read_to_string("test_data/responses/not_enough_credits.json")
|
||||
.expect("Failed to read test data file");
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiSpecific(
|
||||
Seedance2ProSpecificApiError::BillingError { status_code, body }
|
||||
) => {
|
||||
assert_eq!(status_code, StatusCode::BAD_REQUEST);
|
||||
assert!(body.contains("credits not enough"));
|
||||
}
|
||||
other => panic!("Expected BillingError, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn billing_error_case_insensitive() {
|
||||
let body = r#"{"error":"Credits Not Enough"}"#.to_string();
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiSpecific(
|
||||
Seedance2ProSpecificApiError::BillingError { .. }
|
||||
) => {}
|
||||
other => panic!("Expected BillingError, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_error_falls_back_to_uncategorized() {
|
||||
let body = r#"{"error":"something else went wrong"}"#.to_string();
|
||||
|
||||
let error = categorize_seedance2pro_error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
body.clone(),
|
||||
);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body: b }
|
||||
) => {
|
||||
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(b, body);
|
||||
}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_body_falls_back_to_uncategorized() {
|
||||
let error = categorize_seedance2pro_error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
String::new(),
|
||||
);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
) => {}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod categorize_seedance2pro_error;
|
||||
pub mod common_headers;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[
|
||||
{
|
||||
"error": {
|
||||
"json": {
|
||||
"message": "credits not enough",
|
||||
"code": -32600,
|
||||
"data": {
|
||||
"code": "BAD_REQUEST",
|
||||
"httpStatus": 400,
|
||||
"path": "workflow.runTask",
|
||||
"zodError": null
|
||||
},
|
||||
"stack": null
|
||||
},
|
||||
"meta": {
|
||||
"values": {
|
||||
"stack": [
|
||||
"undefined"
|
||||
]
|
||||
},
|
||||
"v": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -22,7 +22,6 @@ workspace-build-acceleration = { workspace = true }
|
||||
# Internal
|
||||
bucket_paths.workspace = true
|
||||
cloud_storage.workspace = true
|
||||
config = { path = "../../../lib/deprecated/config" }
|
||||
easyenv = { path = "../../../lib/easyenv" }
|
||||
enums.workspace = true
|
||||
errors.workspace = true
|
||||
@@ -31,6 +30,7 @@ hashing = { path = "../../../lib/files/hashing" }
|
||||
uuid_utils.workspace = true
|
||||
mimetypes.workspace = true
|
||||
mysql_queries = { path = "../../../schema/database/mysql_queries" }
|
||||
shared_env_var_config.workspace = true
|
||||
storyteller_root = { path = "../../../lib/storyteller_root" }
|
||||
tokens.workspace = true
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ use log::info;
|
||||
use sqlx::{MySql, Pool};
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
|
||||
use config::shared_constants::{DEFAULT_MYSQL_CONNECTION_STRING, DEFAULT_RUST_LOG};
|
||||
use shared_env_var_config::logging::DEFAULT_RUST_LOG;
|
||||
use shared_env_var_config::mysql::env_get_mysql_connection_string_or_default;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
use crate::cli_args::parse_cli_args;
|
||||
@@ -25,10 +26,7 @@ pub async fn main() -> AnyhowResult<()> {
|
||||
// NB: Read secrets (eg. ACCESS_KEY)
|
||||
easyenv::from_filename(".env-secrets")?;
|
||||
|
||||
let db_connection_string =
|
||||
easyenv::get_env_string_or_default(
|
||||
"MYSQL_URL",
|
||||
DEFAULT_MYSQL_CONNECTION_STRING);
|
||||
let db_connection_string = env_get_mysql_connection_string_or_default();
|
||||
|
||||
let pool = MySqlPoolOptions::new()
|
||||
.max_connections(easyenv::get_env_num("MYSQL_MAX_CONNECTIONS", 3)?)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user