mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 17:28:24 -04:00
Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9d5a9bf92 | ||
|
|
640e392d6c | ||
|
|
45651103f0 | ||
|
|
cba8e2c074 | ||
|
|
07d9d537dd | ||
|
|
5770534fc3 | ||
|
|
6fe0909879 | ||
|
|
834f0b48a9 | ||
|
|
d64944b1c3 | ||
|
|
03edd5faf6 | ||
|
|
0ef57ff97e | ||
|
|
adda7483e8 | ||
|
|
e77b946d57 | ||
|
|
3a6fad1812 | ||
|
|
9e04990224 | ||
|
|
885b68630a | ||
|
|
9fee0b1c99 | ||
|
|
631b98f605 | ||
|
|
3334cf7b81 | ||
|
|
50d8abcfb1 | ||
|
|
bce8d166e0 | ||
|
|
5fc27a5153 | ||
|
|
51e5db6b12 | ||
|
|
ff1ce82f66 | ||
|
|
ccea741443 | ||
|
|
2b9882d2d7 | ||
|
|
55cab30dec | ||
|
|
a2f204b1be | ||
|
|
12e4ff7a29 | ||
|
|
8b73569238 | ||
|
|
badabe57eb | ||
|
|
2e62eef85b | ||
|
|
fd9f002615 | ||
|
|
a0e50067bb | ||
|
|
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 |
@@ -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
+25
-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"
|
||||
@@ -3889,6 +3876,13 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error_utils"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errors"
|
||||
version = "0.0.1"
|
||||
@@ -3904,7 +3898,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bootstrap",
|
||||
"chrono",
|
||||
"config",
|
||||
"dotenv",
|
||||
"easyenv",
|
||||
"elasticsearch",
|
||||
@@ -3923,6 +3916,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"server_environment",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"storyteller_root",
|
||||
"tokens",
|
||||
@@ -5764,6 +5758,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"subprocess",
|
||||
"subprocess_common",
|
||||
@@ -6101,7 +6096,6 @@ dependencies = [
|
||||
name = "jwt_signer"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"errors",
|
||||
"hmac 0.10.1",
|
||||
"jwt",
|
||||
"sha2 0.9.9",
|
||||
@@ -6146,13 +6140,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 +6850,6 @@ dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.60",
|
||||
"cloud_storage",
|
||||
"config",
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
@@ -6867,6 +6860,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"storyteller_root",
|
||||
"strum",
|
||||
@@ -8657,16 +8651,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"
|
||||
@@ -10195,9 +10179,9 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bootstrap",
|
||||
"bucket_paths",
|
||||
"chrono",
|
||||
"cloud_storage",
|
||||
"concurrency",
|
||||
"config",
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
@@ -11257,7 +11241,7 @@ dependencies = [
|
||||
"elasticsearch",
|
||||
"elasticsearch_schema",
|
||||
"enums",
|
||||
"enums_public",
|
||||
"error_utils",
|
||||
"errors",
|
||||
"fal_client",
|
||||
"ffmpeg_utils",
|
||||
@@ -11288,7 +11272,6 @@ dependencies = [
|
||||
"pager",
|
||||
"password",
|
||||
"primitives",
|
||||
"prompt_engineering",
|
||||
"r2d2",
|
||||
"rand 0.9.2",
|
||||
"redis 1.0.2",
|
||||
@@ -12888,6 +12871,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"subprocess",
|
||||
"tempdir",
|
||||
@@ -12921,6 +12905,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"tempdir",
|
||||
"tokio",
|
||||
@@ -13377,7 +13362,6 @@ dependencies = [
|
||||
"bucket_paths",
|
||||
"cloud_storage",
|
||||
"concurrency",
|
||||
"config",
|
||||
"easyenv",
|
||||
"errors",
|
||||
"ffmpeg_utils",
|
||||
@@ -13459,6 +13443,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"subprocess",
|
||||
"tempdir",
|
||||
@@ -13489,6 +13474,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"subprocess",
|
||||
"tempdir",
|
||||
@@ -14760,7 +14746,6 @@ dependencies = [
|
||||
"bucket_paths",
|
||||
"cloud_storage",
|
||||
"concurrency",
|
||||
"config",
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
@@ -14773,6 +14758,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"server_environment",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"tokens",
|
||||
"tokio",
|
||||
|
||||
+2
-3
@@ -59,6 +59,7 @@ members = [
|
||||
"crates/lib/data/primitives",
|
||||
"crates/lib/data/web_base64",
|
||||
"crates/lib/easyenv", # NB: Vendored version of https://github.com/echelon/easyenv.rs that we'll improve.
|
||||
"crates/lib/error_utils",
|
||||
"crates/lib/errors",
|
||||
"crates/lib/ffmpeg_utils",
|
||||
"crates/lib/files/file_to_base64_url",
|
||||
@@ -94,7 +95,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 +117,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 +237,7 @@ 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" }
|
||||
error_utils = { path = "crates/lib/error_utils" }
|
||||
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,
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ pub struct GenerateKling16ProImageToVideoRequest {
|
||||
pub duration: Option<GenerateKling16ProDuration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling16ProAspectRatio {
|
||||
/// 16:9 aspect ratio
|
||||
@@ -39,7 +39,7 @@ pub enum GenerateKling16ProAspectRatio {
|
||||
Square,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling16ProDuration {
|
||||
FiveSeconds,
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ pub struct GenerateKling21MasterImageToVideoRequest {
|
||||
pub duration: Option<GenerateKling21MasterDuration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling21MasterAspectRatio {
|
||||
/// 16:9 aspect ratio
|
||||
@@ -35,7 +35,7 @@ pub enum GenerateKling21MasterAspectRatio {
|
||||
Square,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling21MasterDuration {
|
||||
FiveSeconds,
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ pub struct GenerateKling21ProImageToVideoRequest {
|
||||
pub duration: Option<GenerateKling21ProDuration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling21ProAspectRatio {
|
||||
/// 16:9 aspect ratio
|
||||
@@ -39,7 +39,7 @@ pub enum GenerateKling21ProAspectRatio {
|
||||
Square,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling21ProDuration {
|
||||
FiveSeconds,
|
||||
|
||||
+3
-3
@@ -31,14 +31,14 @@ pub struct GenerateSeedance10LiteImageToVideoRequest {
|
||||
pub aspect_ratio: Option<GenerateSeedance10LiteAspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateSeedance10LiteResolution {
|
||||
FourEightyP,
|
||||
SevenTwentyP,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateSeedance10LiteDuration {
|
||||
FiveSeconds,
|
||||
@@ -46,7 +46,7 @@ pub enum GenerateSeedance10LiteDuration {
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateSeedance10LiteAspectRatio {
|
||||
Auto,
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ pub struct GenerateVeo2ImageToVideoRequest {
|
||||
pub duration: Option<GenerateVeo2Duration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo2AspectRatio {
|
||||
Auto,
|
||||
@@ -33,7 +33,7 @@ pub enum GenerateVeo2AspectRatio {
|
||||
TallNineSixteen, // 9:16
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo2Duration {
|
||||
FiveSeconds,
|
||||
|
||||
+2
-2
@@ -27,14 +27,14 @@ pub struct GenerateVeo3FastImageToVideoRequest {
|
||||
pub generate_audio: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3FastResolution {
|
||||
SevenTwentyP, // 720p
|
||||
TenEightyP, // 1080p
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3FastDuration {
|
||||
EightSeconds,
|
||||
|
||||
+3
-3
@@ -37,7 +37,7 @@ pub struct GenerateVeo3ImageToVideoRequest {
|
||||
// pub auto_fix: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3AspectRatio {
|
||||
WideSixteenNine, // 16:9
|
||||
@@ -45,14 +45,14 @@ pub enum GenerateVeo3AspectRatio {
|
||||
Square, // 1:1
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3Resolution {
|
||||
SevenTwentyP, // 720p
|
||||
TenEightyP, // 1080p
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3Duration {
|
||||
EightSeconds,
|
||||
|
||||
+2
-2
@@ -41,14 +41,14 @@ pub struct Kling2p5TurboProMultiFunctionVideoGenRequest {
|
||||
pub aspect_ratio: Option<Kling2p5TurboProMultiFunctionVideoGenAspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Kling2p5TurboProMultiFunctionVideoGenDuration {
|
||||
FiveSeconds,
|
||||
TenSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Kling2p5TurboProMultiFunctionVideoGenAspectRatio {
|
||||
Square,
|
||||
|
||||
+2
-2
@@ -45,14 +45,14 @@ pub struct Kling2p6ProMultiFunctionVideoGenRequest {
|
||||
pub aspect_ratio: Option<Kling2p6ProMultiFunctionVideoGenAspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Kling2p6ProMultiFunctionVideoGenDuration {
|
||||
FiveSeconds,
|
||||
TenSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Kling2p6ProMultiFunctionVideoGenAspectRatio {
|
||||
Square,
|
||||
|
||||
+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>,
|
||||
|
||||
|
||||
+3
-3
@@ -35,14 +35,14 @@ pub struct Sora2MultiFunctionVideoGenRequest {
|
||||
pub aspect_ratio: Option<Sora2MultiFunctionVideoGenAspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Sora2MultiFunctionVideoGenResolution {
|
||||
Auto,
|
||||
SevenTwentyP,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Sora2MultiFunctionVideoGenDuration {
|
||||
FourSeconds,
|
||||
@@ -50,7 +50,7 @@ pub enum Sora2MultiFunctionVideoGenDuration {
|
||||
TwelveSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Sora2MultiFunctionVideoGenAspectRatio {
|
||||
Auto,
|
||||
|
||||
+3
-3
@@ -35,7 +35,7 @@ pub struct Sora2ProMultiFunctionVideoGenRequest {
|
||||
pub aspect_ratio: Option<Sora2ProMultiFunctionVideoGenAspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Sora2ProMultiFunctionVideoGenResolution {
|
||||
Auto,
|
||||
@@ -43,7 +43,7 @@ pub enum Sora2ProMultiFunctionVideoGenResolution {
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Sora2ProMultiFunctionVideoGenDuration {
|
||||
FourSeconds,
|
||||
@@ -51,7 +51,7 @@ pub enum Sora2ProMultiFunctionVideoGenDuration {
|
||||
TwelveSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Sora2ProMultiFunctionVideoGenAspectRatio {
|
||||
Auto,
|
||||
|
||||
+3
-3
@@ -60,7 +60,7 @@ pub struct Veo3p1FastMultiFunctionVideoGenRequest {
|
||||
pub auto_fix: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Veo3p1FastMultiFunctionVideoGenDuration {
|
||||
FourSeconds,
|
||||
@@ -68,7 +68,7 @@ pub enum Veo3p1FastMultiFunctionVideoGenDuration {
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Veo3p1FastMultiFunctionVideoGenAspectRatio {
|
||||
Auto,
|
||||
@@ -76,7 +76,7 @@ pub enum Veo3p1FastMultiFunctionVideoGenAspectRatio {
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Veo3p1FastMultiFunctionVideoGenResolution {
|
||||
SevenTwentyP,
|
||||
|
||||
+3
-3
@@ -60,7 +60,7 @@ pub struct Veo3p1MultiFunctionVideoGenRequest {
|
||||
pub auto_fix: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Veo3p1MultiFunctionVideoGenDuration {
|
||||
FourSeconds,
|
||||
@@ -68,7 +68,7 @@ pub enum Veo3p1MultiFunctionVideoGenDuration {
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Veo3p1MultiFunctionVideoGenAspectRatio {
|
||||
Auto,
|
||||
@@ -76,7 +76,7 @@ pub enum Veo3p1MultiFunctionVideoGenAspectRatio {
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Veo3p1MultiFunctionVideoGenResolution {
|
||||
SevenTwentyP,
|
||||
|
||||
+3
-3
@@ -40,11 +40,11 @@ pub struct OmniGenImageCostAndGenerateRequest {
|
||||
pub image_batch_count: Option<u16>,
|
||||
|
||||
/// Only for angle manipulation models.
|
||||
pub horizontal_angle: Option<f64>,
|
||||
pub adjust_horizontal_angle: Option<f64>,
|
||||
|
||||
/// Only for angle manipulation models.
|
||||
pub vertical_angle: Option<f64>,
|
||||
pub adjust_vertical_angle: Option<f64>,
|
||||
|
||||
/// Only for angle manipulation models.
|
||||
pub zoom: Option<f64>,
|
||||
pub adjust_zoom: Option<f64>,
|
||||
}
|
||||
|
||||
+5
@@ -5,6 +5,7 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_quality::CommonQuality;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Shared request body for both the video cost estimate and video generation endpoints.
|
||||
@@ -39,6 +40,10 @@ pub struct OmniGenVideoCostAndGenerateRequest {
|
||||
/// Reference audio (optional).
|
||||
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>>,
|
||||
|
||||
/// The resolution to use.
|
||||
pub resolution: Option<CommonResolution>,
|
||||
|
||||
|
||||
+2
-1
@@ -103,6 +103,7 @@ pub struct OmniGenImageModelDetails {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution_default: Option<CommonResolution>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quality_options: Option<Vec<CommonQuality>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -124,7 +125,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,
|
||||
|
||||
+14
-3
@@ -1,10 +1,13 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_quality::CommonQuality;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
/// Video model to default to if none is specified
|
||||
const DEFAULT_VIDEO_MODEL : CommonVideoModel = CommonVideoModel::Seedance2p0;
|
||||
|
||||
/// Query string parameters for the video models endpoint.
|
||||
#[derive(Deserialize, IntoParams, ToSchema)]
|
||||
@@ -110,6 +113,12 @@ pub struct OmniGenVideoModelDetails {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub audio_references_max_total_duration_seconds: Option<u16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub character_references_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub character_references_max: Option<u16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub show_generate_with_sound_toggle: Option<bool>,
|
||||
|
||||
@@ -159,7 +168,7 @@ pub struct OmniGenVideoModelDetails {
|
||||
impl Default for OmniGenVideoModelDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
model: DEFAULT_VIDEO_MODEL,
|
||||
full_name: None,
|
||||
text_prompt_supported: None,
|
||||
text_prompt_max_length: None,
|
||||
@@ -176,6 +185,8 @@ impl Default for OmniGenVideoModelDetails {
|
||||
audio_references_supported: None,
|
||||
audio_references_max: None,
|
||||
audio_references_max_total_duration_seconds: None,
|
||||
character_references_supported: None,
|
||||
character_references_max: None,
|
||||
show_generate_with_sound_toggle: None,
|
||||
aspect_ratio_options: None,
|
||||
aspect_ratio_default: 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.
|
||||
}
|
||||
@@ -36,7 +36,11 @@ pub enum CommonAspectRatio {
|
||||
|
||||
// Auto values that bake in resolution
|
||||
// These are from the Seedream models
|
||||
#[serde(rename = "auto_2k")]
|
||||
Auto2k,
|
||||
#[serde(rename = "auto_3k")]
|
||||
Auto3k,
|
||||
#[serde(rename = "auto_4k")]
|
||||
Auto4k,
|
||||
|
||||
// Defined aspect ratios that bake in resolution
|
||||
|
||||
@@ -13,14 +13,16 @@ pub enum CommonImageModel {
|
||||
FluxPro11,
|
||||
#[serde(rename = "flux_pro_1p1_ultra")]
|
||||
FluxPro11Ultra,
|
||||
#[serde(rename = "gpt_image_1")]
|
||||
GptImage1,
|
||||
#[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")]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Common quality levels you can specify when enqueuing a generation.
|
||||
/// Not every model will use this; models that don't simply ignore it.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonQuality {
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
}
|
||||
@@ -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,6 +1,8 @@
|
||||
pub mod audio_list_ref;
|
||||
pub mod character_list_ref;
|
||||
pub mod common_aspect_ratio;
|
||||
pub mod common_image_model;
|
||||
pub mod common_quality;
|
||||
pub mod common_resolution;
|
||||
pub mod common_splat_model;
|
||||
pub mod common_video_model;
|
||||
|
||||
@@ -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
@@ -43,6 +43,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -46,6 +46,7 @@ mod tests {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
|
||||
+11
-7
@@ -6,8 +6,11 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
pub(crate) fn estimate_image_cost_artcraft_flux_pro_1p1(
|
||||
plan: &PlanArtcraftFluxPro11<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// NB(bt): Slightly more than 3 cents, weird megapixel maths. We can eat that cost for now.
|
||||
let cost_per_image: u64 = 3;
|
||||
// Fal pricing: $0.04 per megapixel, billed by rounding up to the nearest
|
||||
// megapixel. The default image_size values are ~1MP, so 4 cents per image.
|
||||
// (The legacy `generate_flux_pro_11_text_to_image_handler` charges this same
|
||||
// amount via `FluxPro11Args::calculate_cost_in_cents`.)
|
||||
let cost_per_image: u64 = 4;
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
GenerateFluxPro11TextToImageNumImages::One => 1,
|
||||
@@ -43,6 +46,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
@@ -60,10 +64,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_usd_cents() {
|
||||
// ~$0.03/image = 3 cents each (slightly more than 3¢ actual, rounded up)
|
||||
assert_eq!(estimate_usd_cents(1), 3);
|
||||
assert_eq!(estimate_usd_cents(2), 6);
|
||||
assert_eq!(estimate_usd_cents(3), 9);
|
||||
assert_eq!(estimate_usd_cents(4), 12);
|
||||
// $0.04/image = 4 cents each (Fal's $0.04/MP at the default ~1MP).
|
||||
assert_eq!(estimate_usd_cents(1), 4);
|
||||
assert_eq!(estimate_usd_cents(2), 8);
|
||||
assert_eq!(estimate_usd_cents(3), 12);
|
||||
assert_eq!(estimate_usd_cents(4), 16);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1::{
|
||||
ArtcraftGptImage1Quality, ArtcraftGptImage1Size, PlanArtcraftGptImage1,
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_gpt_image_1(
|
||||
plan: &PlanArtcraftGptImage1<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing per the fal-ai GPT Image 1 docs, adapted for our artcraft billing:
|
||||
//
|
||||
// Output image cost (per output image):
|
||||
// Low – $0.011 (1024×1024) / $0.016 (other) → 2¢ / 2¢
|
||||
// Medium – $0.042 (1024×1024) / $0.063 (other) → 5¢ / 7¢
|
||||
// High – $0.167 (1024×1024) / $0.250 (other) → 17¢ / 25¢
|
||||
//
|
||||
// Input image cost (edit mode only, per input image, high-fidelity assumed):
|
||||
// 3,050 tokens × $0.005/1K tokens ≈ $0.01525 → 2¢ per input image
|
||||
//
|
||||
// Input text tokens: free (we don't charge for these).
|
||||
//
|
||||
let is_square = matches!(
|
||||
plan.image_size,
|
||||
None | Some(ArtcraftGptImage1Size::Square)
|
||||
);
|
||||
|
||||
let output_cost_per_image: u64 = match (plan.quality, is_square) {
|
||||
(ArtcraftGptImage1Quality::Auto, _) => 17, // Auto treated as High square
|
||||
(ArtcraftGptImage1Quality::Low, true) => 2,
|
||||
(ArtcraftGptImage1Quality::Low, false) => 2,
|
||||
(ArtcraftGptImage1Quality::Medium, true) => 5,
|
||||
(ArtcraftGptImage1Quality::Medium, false) => 7,
|
||||
(ArtcraftGptImage1Quality::High, true) => 17,
|
||||
(ArtcraftGptImage1Quality::High, false) => 25,
|
||||
};
|
||||
|
||||
let num_output_images = plan.num_images.as_u64();
|
||||
let output_cost = output_cost_per_image * num_output_images;
|
||||
|
||||
// Input image token cost: 2¢ per input image (high-fidelity estimate).
|
||||
let input_image_cost: u64 = 2 * plan.num_input_images;
|
||||
|
||||
let cost_in_usd_cents = output_cost + input_image_cost;
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn base() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::GptImage1,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_text(
|
||||
quality: Option<CommonQuality>,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
batch: u16,
|
||||
) -> u64 {
|
||||
let request = GenerateImageRequest {
|
||||
quality,
|
||||
aspect_ratio,
|
||||
image_batch_count: Some(batch),
|
||||
..base()
|
||||
};
|
||||
request.build().unwrap().estimate_costs()
|
||||
.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
fn estimate_edit(
|
||||
quality: Option<CommonQuality>,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
batch: u16,
|
||||
num_input_images: usize,
|
||||
) -> u64 {
|
||||
let tokens: Vec<MediaFileToken> = (0..num_input_images)
|
||||
.map(|i| MediaFileToken::new_from_str(&format!("mf_test{:028}", i)))
|
||||
.collect();
|
||||
let request = GenerateImageRequest {
|
||||
quality,
|
||||
aspect_ratio,
|
||||
image_batch_count: Some(batch),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base()
|
||||
};
|
||||
request.build().unwrap().estimate_costs()
|
||||
.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// ── Default quality (None → High) ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_quality_square_one_image_costs_17_cents() {
|
||||
assert_eq!(estimate_text(None, None, 1), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_quality_square_four_images_costs_68_cents() {
|
||||
assert_eq!(estimate_text(None, None, 4), 68);
|
||||
}
|
||||
|
||||
// ── Low quality (2¢/image regardless of size) ───────────────────────────
|
||||
|
||||
#[test]
|
||||
fn low_square_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), 1), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_wide_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), 1), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_tall_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::TallNineBySixteen), 1), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_four_images() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Low), None, 4), 8);
|
||||
}
|
||||
|
||||
// ── Medium quality (5¢ square, 7¢ wide/tall) ───────────────────────────
|
||||
|
||||
#[test]
|
||||
fn medium_square_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 1), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_wide_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 1), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_tall_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 1), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_square_four_images() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 4), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_wide_four_images() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 4), 28);
|
||||
}
|
||||
|
||||
// ── High quality (17¢ square, 25¢ wide/tall) ───────────────────────────
|
||||
|
||||
#[test]
|
||||
fn high_square_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wide_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 1), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_tall_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), 1), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_four_images() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 4), 68);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wide_four_images() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 4), 100);
|
||||
}
|
||||
|
||||
// ── Edit mode: adds 2¢ per input image ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn edit_one_input_adds_2_cents() {
|
||||
// High square 1 output = 17¢, plus 1 input × 2¢ = 19¢
|
||||
assert_eq!(estimate_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1, 1), 19);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_three_inputs_adds_6_cents() {
|
||||
// High square 1 output = 17¢, plus 3 inputs × 2¢ = 23¢
|
||||
assert_eq!(estimate_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1, 3), 23);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_five_inputs_adds_10_cents() {
|
||||
// Medium wide 2 outputs = 7×2 = 14¢, plus 5 inputs × 2¢ = 24¢
|
||||
assert_eq!(estimate_edit(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 2, 5), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_low_quality_with_inputs() {
|
||||
// Low square 1 output = 2¢, plus 2 inputs × 2¢ = 6¢
|
||||
assert_eq!(estimate_edit(Some(CommonQuality::Low), None, 1, 2), 6);
|
||||
}
|
||||
|
||||
// ── Text mode (no input images) has no input cost ──────────────────────
|
||||
|
||||
#[test]
|
||||
fn text_mode_no_input_cost() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 17);
|
||||
}
|
||||
}
|
||||
+157
-54
@@ -9,42 +9,33 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
pub(crate) fn estimate_image_cost_artcraft_gpt_image_1p5(
|
||||
plan: &PlanArtcraftGptImage1p5<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing varies by quality and image size. 1 credit = 1 USD cent.
|
||||
// Low: $0.01/image (any size)
|
||||
// Medium: $0.03/image (square/unset), $0.05/image (wide or tall)
|
||||
// High: $0.13/image (square/unset), $0.20/image (wide or tall)
|
||||
// Per fal docs (fal-ai/gpt-image-1.5 and fal-ai/gpt-image-1.5/edit):
|
||||
//
|
||||
// From Fal (text to image) -
|
||||
// Output image cost (per output image), rounded up to whole cents:
|
||||
// Low: $0.009 (1024×1024) / $0.013 (other) → 1¢ / 2¢
|
||||
// Medium: $0.034 (1024×1024) / $0.050 (1536×1024) / $0.051 (1024×1536) → 4¢ / 5¢ / 6¢
|
||||
// High: $0.133 (1024×1024) / $0.199 (1536×1024) / $0.200 (1024×1536) → 14¢ / 20¢ / 20¢
|
||||
//
|
||||
// Your request will cost different amounts based on the number of images, quality, and size.
|
||||
// Input text tokens: free (we don't charge for these).
|
||||
//
|
||||
// You will be charged $0.005 per 1,000 input text tokens. One word is roughly 4 tokens.
|
||||
// You will be charged $0.010 per 1,000 output text tokens. The model will consume tokens reasoning about your prompt based on it's complexity.
|
||||
// For low quality, you will be charged $0.009 for 1024x1024 or $0.013 for any other size per image.
|
||||
// For medium quality, you will be charged $0.034 for 1024x1024, $0.051 for 1024x1536 and $0.050 for 1536x1024 per image.
|
||||
// For high quality, you will be charged $0.133 for 1024x1024, $0.200 for 1024x1536 or $0.199 for 1536x1024 per image.
|
||||
// Default quality: High (when request.quality is None).
|
||||
//
|
||||
// From Fal (image to image) -
|
||||
//
|
||||
// Your request will cost different amounts based on the number of images, quality, and size.
|
||||
//
|
||||
// You will be charged $0.005 per 1,000 input text tokens. One word is roughly 4 tokens.
|
||||
// You will be charged $0.008 per 1,000 input image tokens. One 1024x1024 image is roughly 135 tokens in low fidelity mode, or 3,050 tokens in high fidelity mode.
|
||||
// You will be charged $0.010 per 1,000 output text tokens. The model will consume tokens reasoning about your prompt based on it's complexity.
|
||||
// For low quality, you will be charged $0.009 for 1024x1024 or $0.013 for any other size per image.
|
||||
// For medium quality, you will be charged $0.034 for 1024x1024, $0.051 for 1024x1536 and $0.050 for 1536x1024 per image.
|
||||
// For high quality, you will be charged $0.133 for 1024x1024, $0.200 for 1024x1536 or $0.199 for 1536x1024 per image.
|
||||
//
|
||||
let cost_per_image: u64 = match plan.quality {
|
||||
GptImage1p5MultiFunctionImageGenQuality::Low => 1,
|
||||
GptImage1p5MultiFunctionImageGenQuality::Medium => match plan.image_size {
|
||||
Some(GptImage1p5MultiFunctionImageGenSize::Square) | None => 3,
|
||||
Some(GptImage1p5MultiFunctionImageGenSize::Wide) | Some(GptImage1p5MultiFunctionImageGenSize::Tall) => 5,
|
||||
},
|
||||
GptImage1p5MultiFunctionImageGenQuality::High => match plan.image_size {
|
||||
Some(GptImage1p5MultiFunctionImageGenSize::Square) | None => 13,
|
||||
Some(GptImage1p5MultiFunctionImageGenSize::Wide) | Some(GptImage1p5MultiFunctionImageGenSize::Tall) => 20,
|
||||
},
|
||||
use GptImage1p5MultiFunctionImageGenQuality as Q;
|
||||
use GptImage1p5MultiFunctionImageGenSize as S;
|
||||
|
||||
let cost_per_image: u64 = match (plan.quality, plan.image_size) {
|
||||
// Low quality
|
||||
(Q::Low, None | Some(S::Square)) => 1,
|
||||
(Q::Low, Some(S::Wide) | Some(S::Tall)) => 2,
|
||||
|
||||
// Medium quality
|
||||
(Q::Medium, None | Some(S::Square)) => 4,
|
||||
(Q::Medium, Some(S::Wide)) => 5,
|
||||
(Q::Medium, Some(S::Tall)) => 6,
|
||||
|
||||
// High quality
|
||||
(Q::High, None | Some(S::Square)) => 14,
|
||||
(Q::High, Some(S::Wide) | Some(S::Tall)) => 20,
|
||||
};
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
@@ -70,11 +61,16 @@ pub(crate) fn estimate_image_cost_artcraft_gpt_image_1p5(
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
|
||||
fn estimate_usd_cents(image_batch_count: u16, aspect_ratio: Option<CommonAspectRatio>) -> u64 {
|
||||
fn estimate(
|
||||
quality: Option<CommonQuality>,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
batch: u16,
|
||||
) -> u64 {
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::GptImage1p5,
|
||||
provider: Provider::Artcraft,
|
||||
@@ -82,7 +78,8 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
quality,
|
||||
image_batch_count: Some(batch),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
@@ -90,34 +87,140 @@ mod tests {
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
request.build().unwrap().estimate_costs()
|
||||
.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// ── Default quality (None → High) ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_quality_square_one_image() {
|
||||
assert_eq!(estimate(None, Some(CommonAspectRatio::Square), 1), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_medium_square() {
|
||||
// Default quality (Medium) + square/unset size = 3 cents each
|
||||
assert_eq!(estimate_usd_cents(1, None), 3);
|
||||
assert_eq!(estimate_usd_cents(1, Some(CommonAspectRatio::Square)), 3);
|
||||
assert_eq!(estimate_usd_cents(2, Some(CommonAspectRatio::Square)), 6);
|
||||
assert_eq!(estimate_usd_cents(3, Some(CommonAspectRatio::Square)), 9);
|
||||
assert_eq!(estimate_usd_cents(4, Some(CommonAspectRatio::Square)), 12);
|
||||
fn default_quality_unset_one_image() {
|
||||
assert_eq!(estimate(None, None, 1), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_medium_wide() {
|
||||
// Medium + wide = 5 cents each
|
||||
assert_eq!(estimate_usd_cents(1, Some(CommonAspectRatio::WideSixteenByNine)), 5);
|
||||
assert_eq!(estimate_usd_cents(4, Some(CommonAspectRatio::WideSixteenByNine)), 20);
|
||||
fn default_quality_wide_one_image() {
|
||||
assert_eq!(estimate(None, Some(CommonAspectRatio::WideSixteenByNine), 1), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_medium_tall() {
|
||||
// Medium + tall = 5 cents each
|
||||
assert_eq!(estimate_usd_cents(1, Some(CommonAspectRatio::TallNineBySixteen)), 5);
|
||||
assert_eq!(estimate_usd_cents(4, Some(CommonAspectRatio::TallNineBySixteen)), 20);
|
||||
fn default_quality_tall_one_image() {
|
||||
assert_eq!(estimate(None, Some(CommonAspectRatio::TallNineBySixteen), 1), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_quality_square_four_images() {
|
||||
assert_eq!(estimate(None, Some(CommonAspectRatio::Square), 4), 56);
|
||||
}
|
||||
|
||||
// ── Low quality (1¢ square, 2¢ wide/tall) ─────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn low_square_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), 1), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_unset_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Low), None, 1), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_wide_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), 1), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_tall_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Low), Some(CommonAspectRatio::TallNineBySixteen), 1), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_square_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Low), None, 4), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_wide_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), 4), 8);
|
||||
}
|
||||
|
||||
// ── Medium quality (4¢ square, 5¢ wide, 6¢ tall) ─────────────────────
|
||||
|
||||
#[test]
|
||||
fn medium_square_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 1), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_unset_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), None, 1), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_wide_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 1), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_tall_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 1), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_square_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 4), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_wide_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 4), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_tall_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 4), 24);
|
||||
}
|
||||
|
||||
// ── High quality (14¢ square, 20¢ wide/tall) ─────────────────────────
|
||||
|
||||
#[test]
|
||||
fn high_square_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_unset_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), None, 1), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wide_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 1), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_tall_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), 1), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 4), 56);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wide_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 4), 80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_tall_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), 4), 80);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -37,12 +37,13 @@ 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,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+131
-17
@@ -8,12 +8,17 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
pub(crate) fn estimate_image_cost_artcraft_nano_banana_2(
|
||||
plan: &PlanArtcraftNanaBanana2<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.08/image at 0.5K, $0.15/image at 1K or 2K, $0.30/image at 4K.
|
||||
// (Stored as USD cents.)
|
||||
// Pricing (USD cents per image), scaled off the 1K base cost of $0.08:
|
||||
// 0.5K → $0.06 (0.75×)
|
||||
// 1K → $0.08 (1×, default)
|
||||
// 2K → $0.12 (1.5×)
|
||||
// 4K → $0.16 (2×)
|
||||
let cost_per_image: u64 = match plan.resolution {
|
||||
Some(NanaBanana2MultiFunctionImageGenImageResolution::HalfK) => 8,
|
||||
Some(NanaBanana2MultiFunctionImageGenImageResolution::FourK) => 30,
|
||||
_ => 15, // OneK, TwoK, or None (default 1K)
|
||||
Some(NanaBanana2MultiFunctionImageGenImageResolution::HalfK) => 6,
|
||||
Some(NanaBanana2MultiFunctionImageGenImageResolution::OneK) => 8,
|
||||
Some(NanaBanana2MultiFunctionImageGenImageResolution::TwoK) => 12,
|
||||
Some(NanaBanana2MultiFunctionImageGenImageResolution::FourK) => 16,
|
||||
None => 8, // default 1K
|
||||
};
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
@@ -45,12 +50,13 @@ 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,
|
||||
resolution,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
@@ -66,17 +72,125 @@ mod tests {
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_usd_cents() {
|
||||
// 1K/2K: $0.15/image = 15 cents
|
||||
assert_eq!(estimate_usd_cents(None, 1), 15);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 1), 15);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 1), 15);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 2), 30);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 4), 60);
|
||||
// ── Default (None → 1K → 8¢/image) ─────────────────────────────────────────
|
||||
|
||||
// 4K: $0.30/image = 30 cents
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 1), 30);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 2), 60);
|
||||
#[test]
|
||||
fn default_resolution_one_image_costs_8_cents() {
|
||||
assert_eq!(estimate_usd_cents(None, 1), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_resolution_two_images_costs_16_cents() {
|
||||
assert_eq!(estimate_usd_cents(None, 2), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_resolution_three_images_costs_24_cents() {
|
||||
assert_eq!(estimate_usd_cents(None, 3), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_resolution_four_images_costs_32_cents() {
|
||||
assert_eq!(estimate_usd_cents(None, 4), 32);
|
||||
}
|
||||
|
||||
// ── 0.5K → 6¢/image (0.75× base) ───────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn half_k_one_image_costs_6_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::HalfK), 1), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn half_k_two_images_costs_12_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::HalfK), 2), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn half_k_three_images_costs_18_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::HalfK), 3), 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn half_k_four_images_costs_24_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::HalfK), 4), 24);
|
||||
}
|
||||
|
||||
// ── 1K → 8¢/image (base) ───────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn one_k_one_image_costs_8_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 1), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_k_two_images_costs_16_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 2), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_k_three_images_costs_24_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 3), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_k_four_images_costs_32_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 4), 32);
|
||||
}
|
||||
|
||||
// ── 2K → 12¢/image (1.5× base) ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn two_k_one_image_costs_12_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 1), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_k_two_images_costs_24_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 2), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_k_three_images_costs_36_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 3), 36);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_k_four_images_costs_48_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 4), 48);
|
||||
}
|
||||
|
||||
// ── 4K → 16¢/image (2× base) ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn four_k_one_image_costs_16_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 1), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_two_images_costs_32_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 2), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_three_images_costs_48_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 3), 48);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_four_images_costs_64_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 4), 64);
|
||||
}
|
||||
|
||||
// ── 3K (unsupported) falls back to 2K pricing ──────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn three_k_one_image_falls_back_to_two_k_pricing() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::ThreeK), 1), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_k_four_images_falls_back_to_two_k_pricing() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::ThreeK), 4), 48);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -44,12 +44,13 @@ 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,
|
||||
resolution,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -46,6 +46,7 @@ mod tests {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -2,6 +2,7 @@ pub mod estimate_image_cost_artcraft_flux_1_dev;
|
||||
pub mod estimate_image_cost_artcraft_flux_1_schnell;
|
||||
pub mod estimate_image_cost_artcraft_flux_pro_1p1;
|
||||
pub mod estimate_image_cost_artcraft_flux_pro_1p1_ultra;
|
||||
pub mod estimate_image_cost_artcraft_gpt_image_1;
|
||||
pub mod estimate_image_cost_artcraft_gpt_image_1p5;
|
||||
pub mod estimate_image_cost_artcraft_nano_banana;
|
||||
pub mod estimate_image_cost_artcraft_nano_banana_2;
|
||||
|
||||
+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,
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1::PlanFalFluxPro11;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_pro_1p1(
|
||||
plan: &PlanFalFluxPro11<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04 per megapixel, billed by rounding up to nearest megapixel.
|
||||
// Default image sizes are ~1MP (1024x1024), so ~$0.04/image = 4 cents.
|
||||
let cost_per_image: u64 = 4;
|
||||
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,
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::PlanFalFluxPro11Ultra;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_pro_1p1_ultra(
|
||||
plan: &PlanFalFluxPro11Ultra<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.06 per image.
|
||||
let cost_per_image: u64 = 6;
|
||||
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,
|
||||
}
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::{
|
||||
FalGptImage1ImageSize, FalGptImage1Quality, PlanFalGptImage1,
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_gpt_image_1(
|
||||
plan: &PlanFalGptImage1<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Per fal docs (fal-ai/gpt-image-1/{text-to-image,edit-image}):
|
||||
//
|
||||
// Output image cost (per output image):
|
||||
// Low: $0.011 (1024×1024) / $0.016 (other) → 2¢ / 2¢
|
||||
// Medium: $0.042 (1024×1024) / $0.063 (other) → 5¢ / 7¢
|
||||
// High: $0.167 (1024×1024) / $0.250 (other) → 17¢ / 25¢
|
||||
//
|
||||
// Input image tokens (edit mode, per input image, high-fidelity assumed):
|
||||
// 3,050 tokens × $0.005/1K tokens ≈ $0.01525 → 2¢ per input image
|
||||
//
|
||||
// Input text tokens: free (we don't charge for these).
|
||||
//
|
||||
let is_square = matches!(plan.image_size, None | Some(FalGptImage1ImageSize::Square));
|
||||
let output_cost_per_image: u64 = match (plan.quality, is_square) {
|
||||
(FalGptImage1Quality::Low, true) => 2,
|
||||
(FalGptImage1Quality::Low, false) => 2,
|
||||
(FalGptImage1Quality::Medium, true) => 5,
|
||||
(FalGptImage1Quality::Medium, false) => 7,
|
||||
(FalGptImage1Quality::High, true) => 17,
|
||||
(FalGptImage1Quality::High, false) => 25,
|
||||
};
|
||||
|
||||
let num_output_images = plan.num_images.as_u64();
|
||||
let output_cost = output_cost_per_image * num_output_images;
|
||||
|
||||
// Input image token cost: 2¢ per input image (high-fidelity estimate).
|
||||
let input_image_cost: u64 = if plan.image_urls.is_empty() {
|
||||
0
|
||||
} else {
|
||||
2 * plan.image_urls.len() as u64
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = output_cost + input_image_cost;
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::FalGptImage1NumImages;
|
||||
|
||||
fn text_plan(
|
||||
quality: FalGptImage1Quality,
|
||||
image_size: Option<FalGptImage1ImageSize>,
|
||||
num_images: FalGptImage1NumImages,
|
||||
) -> PlanFalGptImage1<'static> {
|
||||
PlanFalGptImage1 {
|
||||
prompt: Some("p"),
|
||||
image_urls: vec![],
|
||||
image_size,
|
||||
quality,
|
||||
num_images,
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_plan(
|
||||
quality: FalGptImage1Quality,
|
||||
image_size: Option<FalGptImage1ImageSize>,
|
||||
num_images: FalGptImage1NumImages,
|
||||
num_input_images: usize,
|
||||
) -> PlanFalGptImage1<'static> {
|
||||
let image_urls = (0..num_input_images)
|
||||
.map(|i| format!("https://fake.example.com/img{}.png", i))
|
||||
.collect();
|
||||
PlanFalGptImage1 {
|
||||
prompt: Some("p"),
|
||||
image_urls,
|
||||
image_size,
|
||||
quality,
|
||||
num_images,
|
||||
}
|
||||
}
|
||||
|
||||
fn cents_text(
|
||||
quality: FalGptImage1Quality,
|
||||
image_size: Option<FalGptImage1ImageSize>,
|
||||
num_images: FalGptImage1NumImages,
|
||||
) -> u64 {
|
||||
estimate_image_cost_fal_gpt_image_1(&text_plan(quality, image_size, num_images))
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn cents_edit(
|
||||
quality: FalGptImage1Quality,
|
||||
image_size: Option<FalGptImage1ImageSize>,
|
||||
num_images: FalGptImage1NumImages,
|
||||
num_input_images: usize,
|
||||
) -> u64 {
|
||||
estimate_image_cost_fal_gpt_image_1(&edit_plan(quality, image_size, num_images, num_input_images))
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// ── Low quality text-to-image (2¢/image, both sizes) ──────────────────
|
||||
|
||||
#[test]
|
||||
fn low_square_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_horizontal_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_vertical_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_unset_size_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Low, None, FalGptImage1NumImages::One), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_four_images() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Low, None, FalGptImage1NumImages::Four), 8);
|
||||
}
|
||||
|
||||
// ── Medium quality text-to-image (5¢ square, 7¢ wide/tall) ────────────
|
||||
|
||||
#[test]
|
||||
fn medium_square_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_horizontal_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_vertical_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_square_four_images() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::Four), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_horizontal_four_images() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Four), 28);
|
||||
}
|
||||
|
||||
// ── High quality text-to-image (17¢ square, 25¢ wide/tall) ────────────
|
||||
|
||||
#[test]
|
||||
fn high_square_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_horizontal_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_vertical_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_two_images() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::Two), 34);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_horizontal_four_images() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Four), 100);
|
||||
}
|
||||
|
||||
// ── Edit mode adds 2¢ per input image ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn edit_high_square_one_output_one_input() {
|
||||
// 17¢ output + 2¢ input = 19¢
|
||||
assert_eq!(cents_edit(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One, 1), 19);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_high_square_one_output_three_inputs() {
|
||||
// 17¢ output + 3×2¢ input = 23¢
|
||||
assert_eq!(cents_edit(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One, 3), 23);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_medium_wide_two_outputs_five_inputs() {
|
||||
// 2×7¢ output + 5×2¢ input = 24¢
|
||||
assert_eq!(cents_edit(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Two, 5), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_low_square_one_output_two_inputs() {
|
||||
// 2¢ output + 2×2¢ input = 6¢
|
||||
assert_eq!(cents_edit(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One, 2), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_high_vertical_four_outputs_one_input() {
|
||||
// 4×25¢ output + 1×2¢ input = 102¢
|
||||
assert_eq!(cents_edit(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::Four, 1), 102);
|
||||
}
|
||||
|
||||
// ── Text mode has no input image cost ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn text_mode_no_input_cost() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 17);
|
||||
}
|
||||
|
||||
// ── Metadata flags ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn metadata_flags_are_default() {
|
||||
let estimate = estimate_image_cost_fal_gpt_image_1(&text_plan(
|
||||
FalGptImage1Quality::Medium,
|
||||
None,
|
||||
FalGptImage1NumImages::One,
|
||||
));
|
||||
assert!(!estimate.is_free);
|
||||
assert!(!estimate.is_unlimited);
|
||||
assert!(!estimate.is_rate_limited);
|
||||
assert!(!estimate.has_watermark);
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::{
|
||||
FalGptImage1p5ImageSize, FalGptImage1p5Quality, PlanFalGptImage1p5,
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_gpt_image_1p5(
|
||||
plan: &PlanFalGptImage1p5<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Per fal docs (fal-ai/gpt-image-1.5 and fal-ai/gpt-image-1.5/edit):
|
||||
//
|
||||
// Output image cost (per output image), rounded up to whole cents:
|
||||
// Low: $0.009 (1024×1024) / $0.013 (other) → 1¢ / 2¢
|
||||
// Medium: $0.034 (1024×1024) / $0.050 (1536×1024) / $0.051 (1024×1536) → 4¢ / 5¢ / 6¢
|
||||
// High: $0.133 (1024×1024) / $0.199 (1536×1024) / $0.200 (1024×1536) → 14¢ / 20¢ / 20¢
|
||||
//
|
||||
use FalGptImage1p5ImageSize as S;
|
||||
|
||||
let cost_per_image: u64 = match (plan.quality, plan.image_size) {
|
||||
// Low quality
|
||||
(FalGptImage1p5Quality::Low, None | Some(S::Square)) => 1,
|
||||
(FalGptImage1p5Quality::Low, Some(S::Wide) | Some(S::Tall)) => 2,
|
||||
|
||||
// Medium quality
|
||||
(FalGptImage1p5Quality::Medium, None | Some(S::Square)) => 4,
|
||||
(FalGptImage1p5Quality::Medium, Some(S::Wide)) => 5,
|
||||
(FalGptImage1p5Quality::Medium, Some(S::Tall)) => 6,
|
||||
|
||||
// High quality
|
||||
(FalGptImage1p5Quality::High, None | Some(S::Square)) => 14,
|
||||
(FalGptImage1p5Quality::High, Some(S::Wide) | Some(S::Tall)) => 20,
|
||||
};
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::FalGptImage1p5NumImages;
|
||||
|
||||
fn plan(
|
||||
quality: FalGptImage1p5Quality,
|
||||
image_size: Option<FalGptImage1p5ImageSize>,
|
||||
num_images: FalGptImage1p5NumImages,
|
||||
) -> PlanFalGptImage1p5<'static> {
|
||||
PlanFalGptImage1p5 {
|
||||
prompt: Some("p"),
|
||||
image_urls: vec![],
|
||||
image_size,
|
||||
quality,
|
||||
num_images,
|
||||
}
|
||||
}
|
||||
|
||||
fn cents(
|
||||
quality: FalGptImage1p5Quality,
|
||||
image_size: Option<FalGptImage1p5ImageSize>,
|
||||
num_images: FalGptImage1p5NumImages,
|
||||
) -> u64 {
|
||||
estimate_image_cost_fal_gpt_image_1p5(&plan(quality, image_size, num_images))
|
||||
.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// ── Low quality (1¢ square, 2¢ wide/tall) ─────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn low_square() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Low, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::One), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_unset() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Low, None, FalGptImage1p5NumImages::One), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_wide() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Low, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::One), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_tall() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Low, Some(FalGptImage1p5ImageSize::Tall), FalGptImage1p5NumImages::One), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_four() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Low, None, FalGptImage1p5NumImages::Four), 4);
|
||||
}
|
||||
|
||||
// ── Medium quality (4¢ square, 5¢ wide, 6¢ tall) ─────────────────────
|
||||
|
||||
#[test]
|
||||
fn medium_square() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::One), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_unset() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, None, FalGptImage1p5NumImages::One), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_wide() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::One), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_tall() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Tall), FalGptImage1p5NumImages::One), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_square_four() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::Four), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_wide_four() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::Four), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_tall_four() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Tall), FalGptImage1p5NumImages::Four), 24);
|
||||
}
|
||||
|
||||
// ── High quality (14¢ square, 20¢ wide/tall) ─────────────────────────
|
||||
|
||||
#[test]
|
||||
fn high_square() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::One), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_unset() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::High, None, FalGptImage1p5NumImages::One), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wide() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::One), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_tall() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Tall), FalGptImage1p5NumImages::One), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_four() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::Four), 56);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wide_four() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::Four), 80);
|
||||
}
|
||||
|
||||
// ── Metadata flags ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn metadata_flags_are_default() {
|
||||
let estimate = estimate_image_cost_fal_gpt_image_1p5(&plan(
|
||||
FalGptImage1p5Quality::Medium, None, FalGptImage1p5NumImages::One,
|
||||
));
|
||||
assert!(!estimate.is_free);
|
||||
assert!(!estimate.is_unlimited);
|
||||
assert!(!estimate.is_rate_limited);
|
||||
assert!(!estimate.has_watermark);
|
||||
}
|
||||
}
|
||||
+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,
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -42,12 +42,13 @@ 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,
|
||||
resolution,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::PlanFalSeedream4;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_seedream_4(
|
||||
plan: &PlanFalSeedream4<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.03 per image.
|
||||
let cost_per_image: u64 = 3;
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::PlanFalSeedream4p5;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_seedream_4p5(
|
||||
plan: &PlanFalSeedream4p5<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04 per image.
|
||||
let cost_per_image: u64 = 4;
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::PlanFalSeedream5Lite;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_seedream_5_lite(
|
||||
plan: &PlanFalSeedream5Lite<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.035 per image — round up to 4 cents.
|
||||
let cost_per_image: u64 = 4;
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
+11
@@ -1 +1,12 @@
|
||||
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_flux_pro_1p1;
|
||||
pub(crate) mod estimate_image_cost_fal_flux_pro_1p1_ultra;
|
||||
pub(crate) mod estimate_image_cost_fal_gpt_image_1;
|
||||
pub(crate) mod estimate_image_cost_fal_gpt_image_1p5;
|
||||
pub(crate) mod estimate_image_cost_fal_seedream_4;
|
||||
pub(crate) mod estimate_image_cost_fal_seedream_4p5;
|
||||
pub(crate) mod estimate_image_cost_fal_seedream_5_lite;
|
||||
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;
|
||||
|
||||
+3
@@ -47,6 +47,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_flux_1_dev_image_request()
|
||||
@@ -69,6 +70,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
..base_flux_1_dev_image_request()
|
||||
@@ -94,6 +96,7 @@ mod tests {
|
||||
println!("--- text-to-image aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a bear walking through an abandoned desert city at night"),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
|
||||
+3
-1
@@ -37,7 +37,6 @@ mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::{base_flux_1_schnell_image_request, get_artcraft_client};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
@@ -47,6 +46,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a man and woman walking through a cyberpunk city at night"),
|
||||
..base_flux_1_schnell_image_request()
|
||||
@@ -69,6 +69,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
..base_flux_1_schnell_image_request()
|
||||
@@ -94,6 +95,7 @@ mod tests {
|
||||
println!("--- text-to-image aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a man and woman walking through a futuristic city at evening"),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
|
||||
+1
@@ -51,6 +51,7 @@ mod tests {
|
||||
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::SquareHd),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
horizontal_angle: Some(30.0),
|
||||
|
||||
+3
@@ -47,6 +47,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_flux_pro_1p1_image_request()
|
||||
@@ -69,6 +70,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
..base_flux_pro_1p1_image_request()
|
||||
@@ -94,6 +96,7 @@ mod tests {
|
||||
println!("--- text-to-image aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a bear walking through an abandoned desert city at night"),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
|
||||
+3
@@ -47,6 +47,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_flux_pro_1p1_ultra_image_request()
|
||||
@@ -69,6 +70,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
..base_flux_pro_1p1_ultra_image_request()
|
||||
@@ -94,6 +96,7 @@ mod tests {
|
||||
println!("--- text-to-image aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a t-rex walking through an abandoned desert city at night"),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
ArtcraftImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1::PlanArtcraftGptImage1;
|
||||
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::GptImage1EditImageRequest;
|
||||
use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageRequest;
|
||||
use artcraft_client::endpoints::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
|
||||
use artcraft_client::endpoints::generate::image::text::generate_gpt_image_1_text_to_image::generate_gpt_image_1_text_to_image;
|
||||
|
||||
/// GPT Image 1 has two distinct legacy storyteller-web endpoints (one for
|
||||
/// text-to-image, one for image-edit), so we dispatch on whether image refs
|
||||
/// are present rather than calling a single multi-function endpoint.
|
||||
pub async fn execute_artcraft_gpt_image_1(
|
||||
plan: &PlanArtcraftGptImage1<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let inference_job_token = match plan.image_inputs {
|
||||
Some(image_inputs) => {
|
||||
let request = GptImage1EditImageRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
image_media_tokens: Some(image_inputs.to_owned()),
|
||||
image_size: plan.image_size.map(|s| s.to_edit()),
|
||||
num_images: Some(plan.num_images.to_edit()),
|
||||
image_quality: Some(plan.quality.to_edit()),
|
||||
};
|
||||
let response = gpt_image_1_edit_image(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
response.inference_job_token
|
||||
}
|
||||
None => {
|
||||
let request = GenerateGptImage1TextToImageRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
num_images: Some(plan.num_images.to_t2i()),
|
||||
image_quality: Some(plan.quality.to_t2i()),
|
||||
};
|
||||
let response = generate_gpt_image_1_text_to_image(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
response.inference_job_token
|
||||
}
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
|
||||
inference_job_token,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::{base_gpt_image_1_image_request, get_artcraft_client};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
// Build-only smoke test that exercises the full text-to-image plan path
|
||||
// (no network I/O — we never call generate_image).
|
||||
#[test]
|
||||
fn build_text_to_image_plan_smoke() {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_batch_count: Some(1),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
let plan = request.build().expect("plan should build");
|
||||
assert!(matches!(plan, ImageGenerationPlan::ArtcraftGptImage1(_)));
|
||||
}
|
||||
|
||||
// Build-only smoke test for the edit path. We can't actually fire the
|
||||
// request without an authenticated client, but we want to make sure the
|
||||
// dispatcher recognizes media-token inputs as edit-mode.
|
||||
#[test]
|
||||
fn build_edit_image_plan_smoke() {
|
||||
let tokens = vec![MediaFileToken::new_from_str("mf_test00000000000000000000000000")];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
let plan = request.build().expect("plan should build");
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = plan else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(plan.image_inputs.is_some(), "edit mode must carry image_inputs");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_image_gpt_image_1() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a horse walking through a cyberpunk city at night"),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
|
||||
println!("Job token: {:?}", payload.inference_job_token);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_edit_image_gpt_image_1() {
|
||||
let client = get_artcraft_client();
|
||||
|
||||
let image_tokens = vec![
|
||||
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
|
||||
];
|
||||
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Change the background to a desert with cacti"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_batch_count: Some(1),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
|
||||
println!("Job token: {:?}", payload.inference_job_token);
|
||||
}
|
||||
}
|
||||
+4
@@ -50,6 +50,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_nano_banana_image_request()
|
||||
@@ -79,6 +80,7 @@ mod tests {
|
||||
prompt: Some("Change the background to a sunny beach"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
..base_nano_banana_image_request()
|
||||
};
|
||||
@@ -103,6 +105,7 @@ mod tests {
|
||||
println!("--- text-to-image aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a shiba walking through a cyberpunk city at night"),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
@@ -138,6 +141,7 @@ mod tests {
|
||||
prompt: Some("Change the background to a sunny beach"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_nano_banana_image_request()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user