mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 00:05:41 -05:00
Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c20e2c8d4e | ||
|
|
6c5d5e37a3 | ||
|
|
4fe6810cba | ||
|
|
615bcee892 | ||
|
|
36df1f780e | ||
|
|
5d7992e8eb | ||
|
|
ffa92e8712 | ||
|
|
d570aa396c | ||
|
|
97087453c3 | ||
|
|
084e1f68b8 | ||
|
|
a1bc472125 | ||
|
|
43fcccce65 | ||
|
|
00132478b1 | ||
|
|
7ea0bb0dd2 | ||
|
|
9645efce7d | ||
|
|
4a3d239735 | ||
|
|
9c0033c673 | ||
|
|
e0e3daa16f | ||
|
|
08f169cebf | ||
|
|
dec59caf11 | ||
|
|
eaf89abb65 | ||
|
|
bd5601ffba | ||
|
|
63dbed7411 | ||
|
|
c0d4f4c7aa | ||
|
|
d234cef007 | ||
|
|
7da69691a0 | ||
|
|
10733d9d23 | ||
|
|
6b0d210b57 |
+102
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `request_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"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": "request_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
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "creator_ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 160
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "creator_set_visibility: enums::common::visibility::Visibility",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | BINARY | ENUM",
|
||||
"char_set": 224,
|
||||
"max_size": 28
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1ddb2207db50e2dbfb37d26c006bffe2d8f5fb011855d5a86d01f395c36df759"
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n ur.id as `id: u64`,\n ur.invited_user_token as `invited_user_token: UserToken`,\n invited.username as `invited_username: String`,\n invited.display_name as `invited_display_name: String`,\n invited.email_address as `invited_email_address: String`,\n ur.referrer_user_token as `referrer_user_token: UserToken`,\n referrer.username as `referrer_username: String`,\n referrer.display_name as `referrer_display_name: String`,\n ur.maybe_referral_code_token as `maybe_referral_code_token: UserReferralCodeToken`,\n ur.maybe_referral_url,\n ur.maybe_landing_url,\n ur.created_at\nFROM user_referrals ur\nJOIN users invited ON invited.token = ur.invited_user_token\nJOIN users referrer ON referrer.token = ur.referrer_user_token\nORDER BY ur.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "invited_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "invited_username: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "invited_display_name: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "invited_email_address: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "referrer_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "referrer_username: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "referrer_display_name: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_referral_code_token: UserReferralCodeToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_referral_url",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_landing_url",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "507a60f98f6da63620abc28a5a83772f29ffcfdfcc8d30bfa7c611d73073ca16"
|
||||
}
|
||||
+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 = ?,\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 maybe_debug_log_event_token = ?,\n\n frontend_failure_category = NULL,\n failure_reason = NULL,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 14
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "99dae5c5f3b3a8e9ce105bff9a9754c3aee92dbf217288c9a7eb925004e3d259"
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n ur.id as `id: u64`,\n ur.invited_user_token as `invited_user_token: UserToken`,\n invited.username as `invited_username: String`,\n invited.display_name as `invited_display_name: String`,\n invited.email_address as `invited_email_address: String`,\n ur.referrer_user_token as `referrer_user_token: UserToken`,\n referrer.username as `referrer_username: String`,\n referrer.display_name as `referrer_display_name: String`,\n ur.maybe_referral_code_token as `maybe_referral_code_token: UserReferralCodeToken`,\n ur.maybe_referral_url,\n ur.maybe_landing_url,\n ur.created_at\nFROM user_referrals ur\nJOIN users invited ON invited.token = ur.invited_user_token\nJOIN users referrer ON referrer.token = ur.referrer_user_token\nWHERE ur.id < ?\nORDER BY ur.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "invited_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "invited_username: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "invited_display_name: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "invited_email_address: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "referrer_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "referrer_username: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "referrer_display_name: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_referral_code_token: UserReferralCodeToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_referral_url",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_landing_url",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b2049fa1c9f76c73a9a22144de716b47dccdd35caeefb3721e75ec9aaf4614f0"
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n ur.id as `id: u64`,\n ur.invited_user_token as `invited_user_token: UserToken`,\n invited.username as `invited_username: String`,\n invited.display_name as `invited_display_name: String`,\n invited.email_address as `invited_email_address: String`,\n ur.referrer_user_token as `referrer_user_token: UserToken`,\n referrer.username as `referrer_username: String`,\n referrer.display_name as `referrer_display_name: String`,\n ur.maybe_referral_code_token as `maybe_referral_code_token: UserReferralCodeToken`,\n ur.maybe_referral_url,\n ur.maybe_landing_url,\n ur.created_at\nFROM user_referrals ur\nJOIN users invited ON invited.token = ur.invited_user_token\nJOIN users referrer ON referrer.token = ur.referrer_user_token\nWHERE referrer.username = ?\nORDER BY ur.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "invited_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "invited_username: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "invited_display_name: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "invited_email_address: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "referrer_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "referrer_username: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "referrer_display_name: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_referral_code_token: UserReferralCodeToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_referral_url",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_landing_url",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e73dfa3af6a1a8f510f084435da86571e096f3d7e597fb01d6397d380b135341"
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n ur.id as `id: u64`,\n ur.invited_user_token as `invited_user_token: UserToken`,\n invited.username as `invited_username: String`,\n invited.display_name as `invited_display_name: String`,\n invited.email_address as `invited_email_address: String`,\n ur.referrer_user_token as `referrer_user_token: UserToken`,\n referrer.username as `referrer_username: String`,\n referrer.display_name as `referrer_display_name: String`,\n ur.maybe_referral_code_token as `maybe_referral_code_token: UserReferralCodeToken`,\n ur.maybe_referral_url,\n ur.maybe_landing_url,\n ur.created_at\nFROM user_referrals ur\nJOIN users invited ON invited.token = ur.invited_user_token\nJOIN users referrer ON referrer.token = ur.referrer_user_token\nWHERE referrer.username = ?\n AND ur.id < ?\nORDER BY ur.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "invited_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "invited_username: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "invited_display_name: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "invited_email_address: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "referrer_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "referrer_username: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "referrer_display_name: String",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_referral_code_token: UserReferralCodeToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_referral_url",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_landing_url",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fb021a5fe7a6c0e284968d917f9d612953629d20a06efccc3fce3d930a7fe266"
|
||||
}
|
||||
Generated
+52
@@ -773,6 +773,7 @@ dependencies = [
|
||||
"artcraft_client",
|
||||
"enums",
|
||||
"fal_client",
|
||||
"gmicloud_client",
|
||||
"log",
|
||||
"muapi_client",
|
||||
"reqwest 0.12.23",
|
||||
@@ -4690,6 +4691,56 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "gmicloud-job"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"actix_helpers",
|
||||
"anyhow",
|
||||
"bootstrap",
|
||||
"bucket_paths",
|
||||
"cloud_storage",
|
||||
"concurrency",
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
"gmicloud_client",
|
||||
"hashing",
|
||||
"jobs_common",
|
||||
"log",
|
||||
"mysql_queries",
|
||||
"pager",
|
||||
"reqwest 0.12.23",
|
||||
"rootly_client",
|
||||
"rootly_config",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"server_environment",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gmicloud_client"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"errors",
|
||||
"log",
|
||||
"reqwest 0.12.23",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"test_data",
|
||||
"test_utils",
|
||||
"tokio",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.18.0"
|
||||
@@ -11279,6 +11330,7 @@ dependencies = [
|
||||
"futures 0.1.31",
|
||||
"futures 0.3.32",
|
||||
"futures-util",
|
||||
"gmicloud_client",
|
||||
"google_sign_in",
|
||||
"hashing",
|
||||
"hostname 0.4.0",
|
||||
|
||||
@@ -14,6 +14,7 @@ members = [
|
||||
"crates/api_clients/google_drive_downloader",
|
||||
"crates/api_clients/google_sign_in",
|
||||
"crates/api_clients/grok_client",
|
||||
"crates/api_clients/gmicloud_client",
|
||||
"crates/api_clients/midjourney_client",
|
||||
"crates/api_clients/muapi_client",
|
||||
"crates/api_clients/beeble_client",
|
||||
@@ -123,6 +124,7 @@ members = [
|
||||
"crates/schema/public/tokens",
|
||||
|
||||
# Services: API servers, jobs, workers, etc.
|
||||
"crates/service/job/gmicloud_job",
|
||||
"crates/service/job/seedance2_pro_job",
|
||||
"crates/service/job/video_thumbnail_job",
|
||||
"crates/service/job/worldlabs_job",
|
||||
@@ -242,6 +244,7 @@ enums = { path = "crates/schema/public/enums" }
|
||||
error_utils = { path = "crates/lib/error_utils" }
|
||||
errors = { path = "crates/lib/errors" }
|
||||
fal_client = { path = "crates/api_clients/fal_client" }
|
||||
gmicloud_client = { path = "crates/api_clients/gmicloud_client" }
|
||||
ffmpeg_utils = { path = "crates/lib/ffmpeg_utils" }
|
||||
file_to_base64_url = { path = "crates/lib/files/file_to_base64_url" }
|
||||
filesys = { path = "crates/lib/files/filesys" }
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
-- Daily active "power users" for ArtCraft since January 1st, 2026.
|
||||
-- A "power user" is a user who created more than 10 ArtCraft inference jobs in the calendar month.
|
||||
-- This query then counts how many such power users were active on each day.
|
||||
|
||||
SELECT
|
||||
DATE(j.created_at) AS day,
|
||||
COUNT(DISTINCT j.maybe_creator_user_token) AS daily_active_power_users
|
||||
FROM generic_inference_jobs j
|
||||
INNER JOIN users u ON u.token = j.maybe_creator_user_token
|
||||
INNER JOIN (
|
||||
SELECT maybe_creator_user_token, DATE_FORMAT(created_at, '%Y-%m') AS month
|
||||
FROM generic_inference_jobs
|
||||
WHERE created_at >= '2026-01-01'
|
||||
AND maybe_creator_user_token IS NOT NULL
|
||||
AND job_type NOT IN (
|
||||
'gpt_sovits', 'tacotron2', 'rvc_v2', 'f5_tts',
|
||||
'so_vits_svc', 'styletts2', 'face_fusion', 'seed_vc', 'comfy_ui'
|
||||
)
|
||||
GROUP BY maybe_creator_user_token, month
|
||||
HAVING COUNT(*) > 10
|
||||
) power ON power.maybe_creator_user_token = j.maybe_creator_user_token
|
||||
AND power.month = DATE_FORMAT(j.created_at, '%Y-%m')
|
||||
WHERE j.created_at >= '2026-01-01'
|
||||
AND j.maybe_creator_user_token IS NOT NULL
|
||||
AND j.job_type NOT IN (
|
||||
'gpt_sovits', 'tacotron2', 'rvc_v2', 'f5_tts',
|
||||
'so_vits_svc', 'styletts2', 'face_fusion', 'seed_vc', 'comfy_ui'
|
||||
)
|
||||
GROUP BY day
|
||||
ORDER BY day DESC;
|
||||
@@ -0,0 +1,24 @@
|
||||
-- Daily active users for ArtCraft since January 1st, 2026.
|
||||
-- A "daily active user" is a user who created at least one ArtCraft inference job that day.
|
||||
|
||||
SELECT
|
||||
DATE(j.created_at) AS day,
|
||||
COUNT(DISTINCT j.maybe_creator_user_token) AS daily_active_users
|
||||
FROM generic_inference_jobs j
|
||||
INNER JOIN users u ON u.token = j.maybe_creator_user_token
|
||||
WHERE j.created_at >= '2026-01-01'
|
||||
AND j.maybe_creator_user_token IS NOT NULL
|
||||
AND j.job_type NOT IN (
|
||||
'gpt_sovits',
|
||||
'tacotron2',
|
||||
'rvc_v2',
|
||||
'f5_tts',
|
||||
'so_vits_svc',
|
||||
'styletts2',
|
||||
'face_fusion',
|
||||
'seed_vc',
|
||||
'comfy_ui'
|
||||
)
|
||||
GROUP BY DATE(j.created_at)
|
||||
ORDER BY day DESC;
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
-- Monthly active "power users" for ArtCraft since January 1st, 2026.
|
||||
-- A "power user" is a user who created more than 10 ArtCraft inference jobs in the calendar month.
|
||||
|
||||
SELECT
|
||||
month,
|
||||
COUNT(*) AS monthly_active_power_users
|
||||
FROM (
|
||||
SELECT
|
||||
maybe_creator_user_token,
|
||||
DATE_FORMAT(j.created_at, '%Y-%m') AS month
|
||||
FROM generic_inference_jobs j
|
||||
INNER JOIN users u ON u.token = j.maybe_creator_user_token
|
||||
WHERE j.created_at >= '2026-01-01'
|
||||
AND j.maybe_creator_user_token IS NOT NULL
|
||||
AND j.job_type NOT IN (
|
||||
'gpt_sovits', 'tacotron2', 'rvc_v2', 'f5_tts',
|
||||
'so_vits_svc', 'styletts2', 'face_fusion', 'seed_vc', 'comfy_ui'
|
||||
)
|
||||
GROUP BY maybe_creator_user_token, month
|
||||
HAVING COUNT(*) > 10
|
||||
) power
|
||||
GROUP BY month
|
||||
ORDER BY month DESC;
|
||||
@@ -0,0 +1,23 @@
|
||||
-- Monthly active users for ArtCraft since January 1st, 2026.
|
||||
-- A "monthly active user" is a user who created at least one ArtCraft inference job that month.
|
||||
|
||||
SELECT
|
||||
DATE_FORMAT(j.created_at, '%Y-%m') AS month,
|
||||
COUNT(DISTINCT j.maybe_creator_user_token) AS monthly_active_users
|
||||
FROM generic_inference_jobs j
|
||||
INNER JOIN users u ON u.token = j.maybe_creator_user_token
|
||||
WHERE j.created_at >= '2026-01-01'
|
||||
AND j.maybe_creator_user_token IS NOT NULL
|
||||
AND j.job_type NOT IN (
|
||||
'gpt_sovits',
|
||||
'tacotron2',
|
||||
'rvc_v2',
|
||||
'f5_tts',
|
||||
'so_vits_svc',
|
||||
'styletts2',
|
||||
'face_fusion',
|
||||
'seed_vc',
|
||||
'comfy_ui'
|
||||
)
|
||||
GROUP BY month
|
||||
ORDER BY month DESC;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
-- Weekly active "power users" for ArtCraft since January 1st, 2026.
|
||||
-- A "power user" is a user who created more than 10 ArtCraft inference jobs in the calendar month.
|
||||
-- This query then counts how many such power users were active in each week.
|
||||
|
||||
SELECT
|
||||
DATE(DATE_SUB(j.created_at, INTERVAL WEEKDAY(j.created_at) DAY)) AS week_start,
|
||||
COUNT(DISTINCT j.maybe_creator_user_token) AS weekly_active_power_users
|
||||
FROM generic_inference_jobs j
|
||||
INNER JOIN users u ON u.token = j.maybe_creator_user_token
|
||||
INNER JOIN (
|
||||
SELECT maybe_creator_user_token, DATE_FORMAT(created_at, '%Y-%m') AS month
|
||||
FROM generic_inference_jobs
|
||||
WHERE created_at >= '2026-01-01'
|
||||
AND maybe_creator_user_token IS NOT NULL
|
||||
AND job_type NOT IN (
|
||||
'gpt_sovits', 'tacotron2', 'rvc_v2', 'f5_tts',
|
||||
'so_vits_svc', 'styletts2', 'face_fusion', 'seed_vc', 'comfy_ui'
|
||||
)
|
||||
GROUP BY maybe_creator_user_token, month
|
||||
HAVING COUNT(*) > 10
|
||||
) power ON power.maybe_creator_user_token = j.maybe_creator_user_token
|
||||
AND power.month = DATE_FORMAT(j.created_at, '%Y-%m')
|
||||
WHERE j.created_at >= '2026-01-01'
|
||||
AND j.maybe_creator_user_token IS NOT NULL
|
||||
AND j.job_type NOT IN (
|
||||
'gpt_sovits', 'tacotron2', 'rvc_v2', 'f5_tts',
|
||||
'so_vits_svc', 'styletts2', 'face_fusion', 'seed_vc', 'comfy_ui'
|
||||
)
|
||||
GROUP BY week_start
|
||||
ORDER BY week_start DESC;
|
||||
@@ -0,0 +1,23 @@
|
||||
-- Weekly active users for ArtCraft since January 1st, 2026.
|
||||
-- A "weekly active user" is a user who created at least one ArtCraft inference job that week.
|
||||
|
||||
SELECT
|
||||
DATE(DATE_SUB(j.created_at, INTERVAL WEEKDAY(j.created_at) DAY)) AS week_start,
|
||||
COUNT(DISTINCT j.maybe_creator_user_token) AS weekly_active_users
|
||||
FROM generic_inference_jobs j
|
||||
INNER JOIN users u ON u.token = j.maybe_creator_user_token
|
||||
WHERE j.created_at >= '2026-01-01'
|
||||
AND j.maybe_creator_user_token IS NOT NULL
|
||||
AND j.job_type NOT IN (
|
||||
'gpt_sovits',
|
||||
'tacotron2',
|
||||
'rvc_v2',
|
||||
'f5_tts',
|
||||
'so_vits_svc',
|
||||
'styletts2',
|
||||
'face_fusion',
|
||||
'seed_vc',
|
||||
'comfy_ui'
|
||||
)
|
||||
GROUP BY week_start
|
||||
ORDER BY week_start DESC;
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod alerts;
|
||||
pub mod jobs;
|
||||
pub mod user;
|
||||
pub mod user_referrals;
|
||||
pub mod wallet_ledger_entries;
|
||||
pub mod wallets;
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::user_referral_codes::UserReferralCodeToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
// --- Request ---
|
||||
|
||||
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||
pub struct ListGlobalUserReferralsQueryParams {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
// --- Response ---
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListGlobalUserReferralsSuccessResponse {
|
||||
pub success: bool,
|
||||
pub referrals: Vec<UserReferralResponse>,
|
||||
pub maybe_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct UserReferralResponse {
|
||||
pub invited_user: InvitedUserDetails,
|
||||
pub referrer_user: ReferrerUserDetails,
|
||||
pub maybe_referral_code_token: Option<UserReferralCodeToken>,
|
||||
pub maybe_referral_url: Option<String>,
|
||||
pub maybe_landing_url: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct InvitedUserDetails {
|
||||
pub token: UserToken,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub email_address: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ReferrerUserDetails {
|
||||
pub token: UserToken,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use crate::moderation::user_referrals::list_global_user_referrals::UserReferralResponse;
|
||||
|
||||
// --- Request ---
|
||||
|
||||
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||
pub struct ListUserReferralsForUserQueryParams {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ListUserReferralsForUserPathInfo {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
// --- Response ---
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListUserReferralsForUserSuccessResponse {
|
||||
pub success: bool,
|
||||
pub referrals: Vec<UserReferralResponse>,
|
||||
pub maybe_cursor: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod list_global_user_referrals;
|
||||
pub mod list_user_referrals_for_user;
|
||||
+6
@@ -23,4 +23,10 @@ pub struct OmniGenImageCostResponse {
|
||||
|
||||
/// Whether the output will have a watermark.
|
||||
pub has_watermark: bool,
|
||||
|
||||
/// Whether failures are refunded.
|
||||
/// True: 100% yes
|
||||
/// False: 100% no
|
||||
/// None: Unknown or variable
|
||||
pub failures_are_refunded: Option<bool>,
|
||||
}
|
||||
|
||||
+7
-1
@@ -18,9 +18,15 @@ pub struct OmniGenVideoCostResponse {
|
||||
/// Whether the user has unlimited generations.
|
||||
pub is_unlimited: bool,
|
||||
|
||||
/// Whether the user is rate limited.
|
||||
/// Whether the user is rate-limited.
|
||||
pub is_rate_limited: bool,
|
||||
|
||||
/// Whether the output will have a watermark.
|
||||
pub has_watermark: bool,
|
||||
|
||||
/// Whether failures are refunded.
|
||||
/// True: 100% yes
|
||||
/// False: 100% no
|
||||
/// None: Unknown or variable
|
||||
pub failures_are_refunded: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ artcraft_api_defs.workspace = true
|
||||
enums.workspace = true
|
||||
artcraft_client.workspace = true
|
||||
fal_client.workspace = true
|
||||
gmicloud_client.workspace = true
|
||||
muapi_client.workspace = true
|
||||
seedance2pro_client.workspace = true
|
||||
tokens.workspace = true
|
||||
|
||||
@@ -41,6 +41,12 @@ pub enum CommonVideoModel {
|
||||
#[serde(rename = "seedance_2p0_fast")]
|
||||
Seedance2p0Fast,
|
||||
|
||||
#[serde(rename = "seedance_2p0_g")]
|
||||
Seedance2p0Global,
|
||||
|
||||
#[serde(rename = "seedance_2p0_fast_g")]
|
||||
Seedance2p0FastGlobal,
|
||||
|
||||
#[serde(rename = "happy_horse_1p0")]
|
||||
HappyHorse1p0,
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use serde_derive::{Deserialize, Serialize};
|
||||
pub enum Provider {
|
||||
Artcraft,
|
||||
Fal,
|
||||
GmiCloud,
|
||||
Muapi,
|
||||
Seedance2Pro,
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ pub mod router_artcraft_client;
|
||||
pub mod router_client;
|
||||
pub mod router_fal_client;
|
||||
pub mod router_fal_webhook_optional_client;
|
||||
pub mod router_gmicloud_client;
|
||||
pub mod router_muapi_client;
|
||||
pub mod router_seedance2pro_client;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_muapi_client::RouterMuapiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::client_error::{ClientError, ClientType};
|
||||
@@ -9,6 +10,7 @@ pub struct MultiRouterClient {
|
||||
pub(crate) artcraft_client: Option<RouterArtcraftClient>,
|
||||
pub(crate) fal_client: Option<RouterFalClient>,
|
||||
pub(crate) fal_webhook_optional_client: Option<RouterFalWebhookOptionalClient>,
|
||||
pub(crate) gmicloud_client: Option<RouterGmiCloudClient>,
|
||||
pub(crate) muapi_client: Option<RouterMuapiClient>,
|
||||
pub(crate) seedance2pro_client: Option<RouterSeedance2ProClient>,
|
||||
}
|
||||
@@ -23,11 +25,17 @@ impl MultiRouterClient {
|
||||
self.fal_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::Fal))
|
||||
}
|
||||
|
||||
pub fn get_fal_webhook_optional_client_ref(&self) -> Result<&RouterFalWebhookOptionalClient, ClientError> {
|
||||
self.fal_webhook_optional_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::FalWebhookOptional))
|
||||
}
|
||||
|
||||
pub fn get_gmicloud_client_ref(&self) -> Result<&RouterGmiCloudClient, ClientError> {
|
||||
self.gmicloud_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::GmiCloud))
|
||||
}
|
||||
|
||||
pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> {
|
||||
self.muapi_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::Muapi))
|
||||
|
||||
+10
-1
@@ -2,6 +2,7 @@ use crate::client::multi_router_client::MultiRouterClient;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_muapi_client::RouterMuapiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
|
||||
@@ -9,6 +10,7 @@ pub struct MultiRouterClientBuilder {
|
||||
artcraft_client: Option<RouterArtcraftClient>,
|
||||
fal_client: Option<RouterFalClient>,
|
||||
fal_webhook_optional_client: Option<RouterFalWebhookOptionalClient>,
|
||||
gmicloud_client: Option<RouterGmiCloudClient>,
|
||||
muapi_client: Option<RouterMuapiClient>,
|
||||
seedance2pro_client: Option<RouterSeedance2ProClient>,
|
||||
}
|
||||
@@ -19,6 +21,7 @@ impl MultiRouterClientBuilder {
|
||||
artcraft_client: None,
|
||||
fal_client: None,
|
||||
fal_webhook_optional_client: None,
|
||||
gmicloud_client: None,
|
||||
muapi_client: None,
|
||||
seedance2pro_client: None,
|
||||
}
|
||||
@@ -33,12 +36,17 @@ impl MultiRouterClientBuilder {
|
||||
self.fal_client = Some(client);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn set_fal_webhook_optional_client(mut self, client: RouterFalWebhookOptionalClient) -> Self {
|
||||
self.fal_webhook_optional_client = Some(client);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_gmicloud_client(mut self, client: RouterGmiCloudClient) -> Self {
|
||||
self.gmicloud_client = Some(client);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_muapi_client(mut self, client: RouterMuapiClient) -> Self {
|
||||
self.muapi_client = Some(client);
|
||||
self
|
||||
@@ -54,6 +62,7 @@ impl MultiRouterClientBuilder {
|
||||
artcraft_client: self.artcraft_client,
|
||||
fal_client: self.fal_client,
|
||||
fal_webhook_optional_client: self.fal_webhook_optional_client,
|
||||
gmicloud_client: self.gmicloud_client,
|
||||
muapi_client: self.muapi_client,
|
||||
seedance2pro_client: self.seedance2pro_client,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::client::multi_router_client::MultiRouterClient;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_muapi_client::RouterMuapiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::client_error::{ClientError, ClientType};
|
||||
@@ -11,6 +12,7 @@ pub enum RouterClient {
|
||||
Artcraft(RouterArtcraftClient),
|
||||
Fal(RouterFalClient),
|
||||
FalWebhookOptional(RouterFalWebhookOptionalClient),
|
||||
GmiCloud(RouterGmiCloudClient),
|
||||
Muapi(RouterMuapiClient),
|
||||
Seedance2Pro(RouterSeedance2ProClient),
|
||||
}
|
||||
@@ -40,6 +42,14 @@ impl RouterClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_gmicloud_client_ref(&self) -> Result<&RouterGmiCloudClient, ClientError> {
|
||||
match self {
|
||||
RouterClient::GmiCloud(client) => Ok(client),
|
||||
RouterClient::Multi(multi) => multi.get_gmicloud_client_ref(),
|
||||
_ => Err(ClientError::ClientNotConfigured(ClientType::GmiCloud)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> {
|
||||
match self {
|
||||
RouterClient::Muapi(client) => Ok(client),
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use gmicloud_client::creds::gmicloud_api_key::GmiCloudApiKey;
|
||||
|
||||
pub struct RouterGmiCloudClient {
|
||||
pub(crate) api_key: GmiCloudApiKey,
|
||||
}
|
||||
|
||||
impl RouterGmiCloudClient {
|
||||
pub fn new(api_key: GmiCloudApiKey) -> Self {
|
||||
RouterGmiCloudClient { api_key }
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub enum ClientType {
|
||||
Artcraft,
|
||||
Fal,
|
||||
FalWebhookOptional,
|
||||
GmiCloud,
|
||||
Muapi,
|
||||
Seedance2Pro,
|
||||
}
|
||||
@@ -18,6 +19,7 @@ impl Display for ClientType {
|
||||
Self::Artcraft => write!(f, "Artcraft"),
|
||||
Self::Fal => write!(f, "Fal"),
|
||||
Self::FalWebhookOptional => write!(f, "FalWebhookOptional"),
|
||||
Self::GmiCloud => write!(f, "GmiCloud"),
|
||||
Self::Muapi => write!(f, "Muapi"),
|
||||
Self::Seedance2Pro => write!(f, "Seedance2Pro"),
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use artcraft_client::error::storyteller_error::StorytellerError;
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use gmicloud_client::error::gmicloud_error::GmiCloudError;
|
||||
use muapi_client::error::muapi_error::MuapiError;
|
||||
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
|
||||
|
||||
@@ -9,6 +10,7 @@ use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
|
||||
pub enum ProviderError {
|
||||
Storyteller(StorytellerError),
|
||||
Fal(FalErrorPlus),
|
||||
GmiCloud(GmiCloudError),
|
||||
Muapi(MuapiError),
|
||||
Seedance2Pro(Seedance2ProError),
|
||||
}
|
||||
@@ -20,6 +22,7 @@ impl Display for ProviderError {
|
||||
match self {
|
||||
Self::Storyteller(e) => write!(f, "Storyteller provider error: {}", e),
|
||||
Self::Fal(e) => write!(f, "Fal provider error: {}", e),
|
||||
Self::GmiCloud(e) => write!(f, "GmiCloud provider error: {}", e),
|
||||
Self::Muapi(e) => write!(f, "Muapi provider error: {}", e),
|
||||
Self::Seedance2Pro(e) => write!(f, "Seedance2Pro provider error: {}", e),
|
||||
}
|
||||
@@ -38,6 +41,12 @@ impl From<FalErrorPlus> for ProviderError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GmiCloudError> for ProviderError {
|
||||
fn from(error: GmiCloudError) -> Self {
|
||||
Self::GmiCloud(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MuapiError> for ProviderError {
|
||||
fn from(error: MuapiError) -> Self {
|
||||
Self::Muapi(error)
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ pub(crate) fn estimate_image_cost_artcraft_flux_1_dev(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ pub(crate) fn estimate_image_cost_artcraft_flux_1_schnell(
|
||||
is_unlimited: true,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ pub(crate) fn estimate_image_cost_artcraft_flux_2_lora_angles(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ pub(crate) fn estimate_image_cost_artcraft_flux_pro_1p1(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ pub(crate) fn estimate_image_cost_artcraft_flux_pro_1p1_ultra(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -48,6 +48,7 @@ pub(crate) fn estimate_image_cost_artcraft_gpt_image_1(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ pub(crate) fn estimate_image_cost_artcraft_gpt_image_1p5(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ pub(crate) fn estimate_image_cost_artcraft_gpt_image_2(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ pub(crate) fn estimate_image_cost_artcraft_nano_banana(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -37,6 +37,7 @@ pub(crate) fn estimate_image_cost_artcraft_nano_banana_2(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ pub(crate) fn estimate_image_cost_artcraft_nano_banana_pro(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ pub(crate) fn estimate_image_cost_artcraft_qwen_edit_2511_angles(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ pub(crate) fn estimate_image_cost_artcraft_seedream_4(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ pub(crate) fn estimate_image_cost_artcraft_seedream_4p5(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ pub(crate) fn estimate_image_cost_artcraft_seedream_5_lite(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -22,5 +22,6 @@ pub(crate) fn estimate_image_cost_fal_flux_1_dev(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -22,5 +22,6 @@ pub(crate) fn estimate_image_cost_fal_flux_1_schnell(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -17,5 +17,6 @@ pub(crate) fn estimate_image_cost_fal_flux_pro_1p1(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -16,5 +16,6 @@ pub(crate) fn estimate_image_cost_fal_flux_pro_1p1_ultra(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ pub(crate) fn estimate_image_cost_fal_gpt_image_1(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -38,6 +38,7 @@ pub(crate) fn estimate_image_cost_fal_gpt_image_1p5(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ pub(crate) fn estimate_image_cost_fal_gpt_image_2(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -30,5 +30,6 @@ pub(crate) fn estimate_image_cost_fal_nano_banana(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -40,5 +40,6 @@ pub(crate) fn estimate_image_cost_fal_nano_banana_2(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ pub(crate) fn estimate_image_cost_fal_nano_banana_pro(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -15,5 +15,6 @@ pub(crate) fn estimate_image_cost_fal_seedream_4(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -15,5 +15,6 @@ pub(crate) fn estimate_image_cost_fal_seedream_4p5(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -15,5 +15,6 @@ pub(crate) fn estimate_image_cost_fal_seedream_5_lite(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -12,6 +12,9 @@ use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::build::build_fal_flux_1_dev;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::build::build_fal_flux_1_schnell;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::build::build_fal_gpt_image_1;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::build::build_fal_gpt_image_1p5;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::build::build_fal_gpt_image_2;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::build::build_fal_nano_banana_2;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::build::build_fal_nano_banana_pro;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::plan_generate_image_artcraft_flux_1_dev;
|
||||
@@ -99,6 +102,9 @@ impl GenerateImageRequestBuilder {
|
||||
match (self.provider, self.model) {
|
||||
(Provider::Fal, CommonImageModel::Flux1Dev) => true,
|
||||
(Provider::Fal, CommonImageModel::Flux1Schnell) => true,
|
||||
(Provider::Fal, CommonImageModel::GptImage1) => true,
|
||||
(Provider::Fal, CommonImageModel::GptImage1p5) => true,
|
||||
(Provider::Fal, CommonImageModel::GptImage2) => true,
|
||||
(Provider::Fal, CommonImageModel::NanoBanana2) => true,
|
||||
(Provider::Fal, CommonImageModel::NanoBananaPro) => true,
|
||||
_ => false,
|
||||
@@ -109,6 +115,9 @@ impl GenerateImageRequestBuilder {
|
||||
match (self.provider, self.model) {
|
||||
(Provider::Fal, CommonImageModel::Flux1Dev) => build_fal_flux_1_dev(self),
|
||||
(Provider::Fal, CommonImageModel::Flux1Schnell) => build_fal_flux_1_schnell(self),
|
||||
(Provider::Fal, CommonImageModel::GptImage1) => build_fal_gpt_image_1(self),
|
||||
(Provider::Fal, CommonImageModel::GptImage1p5) => build_fal_gpt_image_1p5(self),
|
||||
(Provider::Fal, CommonImageModel::GptImage2) => build_fal_gpt_image_2(self),
|
||||
(Provider::Fal, CommonImageModel::NanoBanana2) => build_fal_nano_banana_2(self),
|
||||
(Provider::Fal, CommonImageModel::NanoBananaPro) => build_fal_nano_banana_pro(self),
|
||||
_ => self.unsupported_provider_and_model(),
|
||||
|
||||
+6
@@ -7,4 +7,10 @@ pub struct ImageGenerationCostEstimate {
|
||||
pub is_unlimited: bool,
|
||||
pub is_rate_limited: bool,
|
||||
pub has_watermark: bool,
|
||||
|
||||
/// Whether failures are refunded.
|
||||
/// True: 100% yes
|
||||
/// False: 100% no
|
||||
/// None: Unknown or variable
|
||||
pub failures_are_refunded: Option<bool>,
|
||||
}
|
||||
|
||||
-11
@@ -183,17 +183,6 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_media_tokens() {
|
||||
let tokens = vec![make_token(), make_token()];
|
||||
let request = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_none_is_none() {
|
||||
let tokens = vec![make_token()];
|
||||
|
||||
-11
@@ -185,17 +185,6 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_media_tokens() {
|
||||
let tokens = vec![make_token(), make_token()];
|
||||
let request = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_none_is_none() {
|
||||
let tokens = vec![make_token()];
|
||||
|
||||
+33
@@ -7,6 +7,12 @@ use crate::generate::generate_image_v2::providers::fal::flux_1_dev::cost::FalFlu
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::request::FalFlux1DevRequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::cost::FalFlux1SchnellCostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::request::FalFlux1SchnellRequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::cost::FalGptImage1CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::request::FalGptImage1RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::cost::FalGptImage1p5CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::request::FalGptImage1p5RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::cost::FalGptImage2CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::request::FalGptImage2RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::cost::FalNanoBanana2CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::request::FalNanoBanana2RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::cost::FalNanoBananaProCostState;
|
||||
@@ -16,6 +22,9 @@ use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::request
|
||||
pub enum ImageGenerationRequest {
|
||||
FalFlux1Dev(FalFlux1DevRequestState),
|
||||
FalFlux1Schnell(FalFlux1SchnellRequestState),
|
||||
FalGptImage1(FalGptImage1RequestState),
|
||||
FalGptImage1p5(FalGptImage1p5RequestState),
|
||||
FalGptImage2(FalGptImage2RequestState),
|
||||
FalNanoBanana2(FalNanoBanana2RequestState),
|
||||
FalNanoBananaPro(FalNanoBananaProRequestState),
|
||||
}
|
||||
@@ -25,6 +34,9 @@ impl ImageGenerationRequest {
|
||||
match self {
|
||||
Self::FalFlux1Dev(_) => Provider::Fal,
|
||||
Self::FalFlux1Schnell(_) => Provider::Fal,
|
||||
Self::FalGptImage1(_) => Provider::Fal,
|
||||
Self::FalGptImage1p5(_) => Provider::Fal,
|
||||
Self::FalGptImage2(_) => Provider::Fal,
|
||||
Self::FalNanoBanana2(_) => Provider::Fal,
|
||||
Self::FalNanoBananaPro(_) => Provider::Fal,
|
||||
}
|
||||
@@ -38,6 +50,15 @@ impl ImageGenerationRequest {
|
||||
Self::FalFlux1Schnell(request) => {
|
||||
Ok(FalFlux1SchnellCostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalGptImage1(request) => {
|
||||
Ok(FalGptImage1CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalGptImage1p5(request) => {
|
||||
Ok(FalGptImage1p5CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalGptImage2(request) => {
|
||||
Ok(FalGptImage2CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalNanoBanana2(request) => {
|
||||
Ok(FalNanoBanana2CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
@@ -57,6 +78,18 @@ impl ImageGenerationRequest {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalGptImage1(request) => {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalGptImage1p5(request) => {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalGptImage2(request) => {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalNanoBanana2(request) => {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
|
||||
+2
-1
@@ -23,7 +23,8 @@ impl FalFlux1DevCostState {
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ impl FalFlux1SchnellCostState {
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
use fal_client::requests::api::image::edit::gpt_image_1_edit_image::api::{
|
||||
GptImage1EditImageNumImages, GptImage1EditImageQuality, GptImage1EditImageRequest,
|
||||
GptImage1EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_1_text_to_image::api::{
|
||||
GptImage1TextToImageNumImages, GptImage1TextToImageQuality,
|
||||
GptImage1TextToImageRequest, GptImage1TextToImageSize,
|
||||
};
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::request::FalGptImage1RequestState;
|
||||
|
||||
pub fn build_fal_gpt_image_1(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let prompt = builder.prompt.clone().unwrap_or_default();
|
||||
let image_urls = resolve_image_urls(builder.image_inputs.clone())?;
|
||||
let num_images = plan_num_images(builder.image_batch_count, builder.request_mismatch_mitigation_strategy)?;
|
||||
let image_size = plan_image_size(builder.aspect_ratio);
|
||||
let quality = plan_quality(builder.quality);
|
||||
|
||||
let state = if image_urls.is_empty() {
|
||||
FalGptImage1RequestState::TextToImage(GptImage1TextToImageRequest {
|
||||
prompt,
|
||||
num_images: to_t2i_num_images(num_images),
|
||||
image_size: image_size.map(to_t2i_image_size),
|
||||
quality: Some(to_t2i_quality(quality)),
|
||||
background: None,
|
||||
output_format: None,
|
||||
})
|
||||
} else {
|
||||
FalGptImage1RequestState::EditImage(GptImage1EditImageRequest {
|
||||
prompt,
|
||||
image_urls,
|
||||
num_images: to_edit_num_images(num_images),
|
||||
mask_image_url: None,
|
||||
image_size: image_size.map(to_edit_image_size),
|
||||
quality: Some(to_edit_quality(quality)),
|
||||
input_fidelity: None,
|
||||
background: None,
|
||||
output_format: None,
|
||||
})
|
||||
};
|
||||
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage1(state),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedQuality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedImageSize {
|
||||
Square,
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<PlannedNumImages, ArtcraftRouterError> {
|
||||
let count = count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(PlannedNumImages::One),
|
||||
2 => Ok(PlannedNumImages::Two),
|
||||
3 => Ok(PlannedNumImages::Three),
|
||||
4 => Ok(PlannedNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(PlannedNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_quality(quality: Option<CommonQuality>) -> PlannedQuality {
|
||||
match quality {
|
||||
Some(CommonQuality::Low) => PlannedQuality::Low,
|
||||
Some(CommonQuality::Medium) => PlannedQuality::Medium,
|
||||
Some(CommonQuality::High) | None => PlannedQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<PlannedImageSize> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => None,
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Some(PlannedImageSize::Square)
|
||||
}
|
||||
Some(CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(CommonAspectRatio::WideFourByThree)
|
||||
| Some(CommonAspectRatio::WideFiveByFour)
|
||||
| Some(CommonAspectRatio::WideSixteenByNine)
|
||||
| Some(CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(CommonAspectRatio::Wide) => Some(PlannedImageSize::Horizontal),
|
||||
Some(CommonAspectRatio::TallTwoByThree)
|
||||
| Some(CommonAspectRatio::TallThreeByFour)
|
||||
| Some(CommonAspectRatio::TallFourByFive)
|
||||
| Some(CommonAspectRatio::TallNineBySixteen)
|
||||
| Some(CommonAspectRatio::TallNineByTwentyOne)
|
||||
| Some(CommonAspectRatio::Tall) => Some(PlannedImageSize::Vertical),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_num_images(n: PlannedNumImages) -> GptImage1TextToImageNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => GptImage1TextToImageNumImages::One,
|
||||
PlannedNumImages::Two => GptImage1TextToImageNumImages::Two,
|
||||
PlannedNumImages::Three => GptImage1TextToImageNumImages::Three,
|
||||
PlannedNumImages::Four => GptImage1TextToImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_num_images(n: PlannedNumImages) -> GptImage1EditImageNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => GptImage1EditImageNumImages::One,
|
||||
PlannedNumImages::Two => GptImage1EditImageNumImages::Two,
|
||||
PlannedNumImages::Three => GptImage1EditImageNumImages::Three,
|
||||
PlannedNumImages::Four => GptImage1EditImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_quality(q: PlannedQuality) -> GptImage1TextToImageQuality {
|
||||
match q {
|
||||
PlannedQuality::Low => GptImage1TextToImageQuality::Low,
|
||||
PlannedQuality::Medium => GptImage1TextToImageQuality::Medium,
|
||||
PlannedQuality::High => GptImage1TextToImageQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_quality(q: PlannedQuality) -> GptImage1EditImageQuality {
|
||||
match q {
|
||||
PlannedQuality::Low => GptImage1EditImageQuality::Low,
|
||||
PlannedQuality::Medium => GptImage1EditImageQuality::Medium,
|
||||
PlannedQuality::High => GptImage1EditImageQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_image_size(s: PlannedImageSize) -> GptImage1TextToImageSize {
|
||||
match s {
|
||||
PlannedImageSize::Square => GptImage1TextToImageSize::Square,
|
||||
PlannedImageSize::Horizontal => GptImage1TextToImageSize::Horizontal,
|
||||
PlannedImageSize::Vertical => GptImage1TextToImageSize::Vertical,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_image_size(s: PlannedImageSize) -> GptImage1EditImageSize {
|
||||
match s {
|
||||
PlannedImageSize::Square => GptImage1EditImageSize::Square,
|
||||
PlannedImageSize::Horizontal => GptImage1EditImageSize::Horizontal,
|
||||
PlannedImageSize::Vertical => GptImage1EditImageSize::Vertical,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_urls(
|
||||
image_inputs: Option<ImageListRef>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::GptImage1,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_t2i(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> GptImage1TextToImageRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage1(
|
||||
FalGptImage1RequestState::TextToImage(req)
|
||||
)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected GPT Image 1 text-to-image request")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
fn unwrap_edit(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> GptImage1EditImageRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage1(
|
||||
FalGptImage1RequestState::EditImage(req)
|
||||
)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected GPT Image 1 edit-image request")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
fn assert_debug<T: Debug>(actual: T, expected: &str) {
|
||||
assert_eq!(format!("{:?}", actual), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build2_routes_to_gpt_image_1_request() {
|
||||
let req = unwrap_t2i(base_builder().build2());
|
||||
assert_eq!(req.prompt, "a cat in space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_detection_is_based_on_image_urls() {
|
||||
let text_req = unwrap_t2i(build_fal_gpt_image_1(base_builder()));
|
||||
assert_eq!(text_req.prompt, "a cat in space");
|
||||
|
||||
let edit_req = unwrap_edit(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_eq!(edit_req.image_urls, vec!["https://example.com/img.jpg"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_file_tokens_are_rejected() {
|
||||
let result = build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(vec![])),
|
||||
..base_builder()
|
||||
});
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_maps_exhaustively_for_text_and_edit() {
|
||||
let text_cases = [
|
||||
(1, "One"),
|
||||
(2, "Two"),
|
||||
(3, "Three"),
|
||||
(4, "Four"),
|
||||
];
|
||||
|
||||
for (count, expected) in text_cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(count),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, expected);
|
||||
}
|
||||
|
||||
for (count, expected) in text_cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
image_batch_count: Some(count),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_rejects_zero_and_handles_overflow_by_strategy() {
|
||||
assert!(matches!(
|
||||
build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(0),
|
||||
..base_builder()
|
||||
}),
|
||||
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
|
||||
));
|
||||
assert!(matches!(
|
||||
build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(5),
|
||||
..base_builder()
|
||||
}),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, "Four");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "Some(High)"),
|
||||
(Some(CommonQuality::Low), "Some(Low)"),
|
||||
(Some(CommonQuality::Medium), "Some(Medium)"),
|
||||
(Some(CommonQuality::High), "Some(High)"),
|
||||
];
|
||||
|
||||
for (quality, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
quality,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.quality, expected);
|
||||
}
|
||||
|
||||
for (quality, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
quality,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.quality, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "None"),
|
||||
(Some(CommonAspectRatio::Auto), "None"),
|
||||
(Some(CommonAspectRatio::Auto2k), "None"),
|
||||
(Some(CommonAspectRatio::Auto3k), "None"),
|
||||
(Some(CommonAspectRatio::Auto4k), "None"),
|
||||
(Some(CommonAspectRatio::Square), "Some(Square)"),
|
||||
(Some(CommonAspectRatio::SquareHd), "Some(Square)"),
|
||||
(Some(CommonAspectRatio::WideThreeByTwo), "Some(Horizontal)"),
|
||||
(Some(CommonAspectRatio::WideFourByThree), "Some(Horizontal)"),
|
||||
(Some(CommonAspectRatio::WideFiveByFour), "Some(Horizontal)"),
|
||||
(Some(CommonAspectRatio::WideSixteenByNine), "Some(Horizontal)"),
|
||||
(Some(CommonAspectRatio::WideTwentyOneByNine), "Some(Horizontal)"),
|
||||
(Some(CommonAspectRatio::Wide), "Some(Horizontal)"),
|
||||
(Some(CommonAspectRatio::TallTwoByThree), "Some(Vertical)"),
|
||||
(Some(CommonAspectRatio::TallThreeByFour), "Some(Vertical)"),
|
||||
(Some(CommonAspectRatio::TallFourByFive), "Some(Vertical)"),
|
||||
(Some(CommonAspectRatio::TallNineBySixteen), "Some(Vertical)"),
|
||||
(Some(CommonAspectRatio::TallNineByTwentyOne), "Some(Vertical)"),
|
||||
(Some(CommonAspectRatio::Tall), "Some(Vertical)"),
|
||||
];
|
||||
|
||||
for (aspect_ratio, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
aspect_ratio,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.image_size, expected);
|
||||
}
|
||||
|
||||
for (aspect_ratio, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
aspect_ratio,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.image_size, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::request::FalGptImage1RequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalGptImage1CostState {
|
||||
request: FalGptImage1RequestState,
|
||||
}
|
||||
|
||||
impl FalGptImage1CostState {
|
||||
pub fn from_request(request: &FalGptImage1RequestState) -> Self {
|
||||
Self { request: request.clone() }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = match &self.request {
|
||||
FalGptImage1RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage1RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
};
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use fal_client::requests::api::image::edit::gpt_image_1_edit_image::api::{GptImage1EditImageNumImages, GptImage1EditImageQuality, GptImage1EditImageRequest, GptImage1EditImageSize};
|
||||
use fal_client::requests::api::image::text::gpt_image_1_text_to_image::api::{GptImage1TextToImageNumImages, GptImage1TextToImageQuality, GptImage1TextToImageRequest, GptImage1TextToImageSize};
|
||||
|
||||
#[test]
|
||||
fn text_to_image_cost_table_covers_quality_size_and_count() {
|
||||
for (quality, image_size, expected_by_count) in text_to_image_price_cases() {
|
||||
for (num_images, expected) in text_to_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage1RequestState::TextToImage(GptImage1TextToImageRequest { prompt: "test".to_string(), num_images, image_size, quality, background: None, output_format: None });
|
||||
let cost = FalGptImage1CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_image_cost_table_covers_quality_size_and_count() {
|
||||
for (quality, image_size, expected_by_count) in edit_image_price_cases() {
|
||||
for (num_images, expected) in edit_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage1RequestState::EditImage(GptImage1EditImageRequest { prompt: "test".to_string(), image_urls: vec!["https://example.com/image.png".to_string()], num_images, mask_image_url: None, image_size, quality, input_fidelity: None, background: None, output_format: None });
|
||||
let cost = FalGptImage1CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn text_to_image_price_cases() -> [(Option<GptImage1TextToImageQuality>, Option<GptImage1TextToImageSize>, [u64; 4]); 13] {
|
||||
[
|
||||
(None, None, [5, 10, 15, 20]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Auto), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Square), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Horizontal), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Vertical), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Auto), [5, 10, 15, 20]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Square), [5, 10, 15, 20]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Horizontal), [7, 14, 21, 28]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Vertical), [7, 14, 21, 28]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Auto), [17, 34, 51, 68]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Square), [17, 34, 51, 68]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Horizontal), [25, 50, 75, 100]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Vertical), [25, 50, 75, 100]),
|
||||
]
|
||||
}
|
||||
|
||||
fn edit_image_price_cases() -> [(Option<GptImage1EditImageQuality>, Option<GptImage1EditImageSize>, [u64; 4]); 13] {
|
||||
[
|
||||
(None, None, [5, 10, 15, 20]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Auto), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Square), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Horizontal), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Vertical), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Auto), [5, 10, 15, 20]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Square), [5, 10, 15, 20]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Horizontal), [7, 14, 21, 28]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Vertical), [7, 14, 21, 28]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Auto), [17, 34, 51, 68]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Square), [17, 34, 51, 68]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Horizontal), [25, 50, 75, 100]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Vertical), [25, 50, 75, 100]),
|
||||
]
|
||||
}
|
||||
|
||||
fn text_to_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage1TextToImageNumImages, u64); 4] {
|
||||
[(GptImage1TextToImageNumImages::One, expected_by_count[0]), (GptImage1TextToImageNumImages::Two, expected_by_count[1]), (GptImage1TextToImageNumImages::Three, expected_by_count[2]), (GptImage1TextToImageNumImages::Four, expected_by_count[3])]
|
||||
}
|
||||
|
||||
fn edit_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage1EditImageNumImages, u64); 4] {
|
||||
[(GptImage1EditImageNumImages::One, expected_by_count[0]), (GptImage1EditImageNumImages::Two, expected_by_count[1]), (GptImage1EditImageNumImages::Three, expected_by_count[2]), (GptImage1EditImageNumImages::Four, expected_by_count[3])]
|
||||
}
|
||||
|
||||
fn assert_standard_cost_estimate(cost: ImageGenerationCostEstimate, expected_usd_cents: u64) {
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(expected_usd_cents));
|
||||
assert_eq!(cost.cost_in_credits, None);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::api::image::edit::gpt_image_1_edit_image::api::GptImage1EditImageRequest;
|
||||
use fal_client::requests::api::image::text::gpt_image_1_text_to_image::api::GptImage1TextToImageRequest;
|
||||
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::{FalImageResponsePayload, GenerateImageResponse};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FalGptImage1RequestState {
|
||||
TextToImage(GptImage1TextToImageRequest),
|
||||
EditImage(GptImage1EditImageRequest),
|
||||
}
|
||||
|
||||
impl FalGptImage1RequestState {
|
||||
pub async fn send(&self, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
Self::TextToImage(request) => send_request(request, client).await,
|
||||
Self::EditImage(request) => send_request(request, client).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FalResponseIds {
|
||||
request_id: Option<String>,
|
||||
gateway_request_id: Option<String>,
|
||||
status_url: Option<String>,
|
||||
response_url: Option<String>,
|
||||
}
|
||||
|
||||
async fn send_request<T>(request: &T, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError>
|
||||
where
|
||||
T: FalEndpoint + Clone + Debug + Send + Sync + 'static,
|
||||
{
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
|
||||
let payload = send_fal_request(request, client).await?;
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: payload.request_id,
|
||||
gateway_request_id: payload.gateway_request_id,
|
||||
maybe_status_url: payload.status_url,
|
||||
maybe_response_url: payload.response_url,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn send_fal_request<T: FalEndpoint>(
|
||||
request: &T,
|
||||
client: &RouterFalWebhookOptionalClient,
|
||||
) -> Result<FalResponseIds, ArtcraftRouterError> {
|
||||
if let Some(webhook_url) = &client.webhook_url {
|
||||
let response = request.send_webhook_request(&client.api_key, webhook_url).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: response.request_id,
|
||||
gateway_request_id: response.gateway_request_id,
|
||||
status_url: None,
|
||||
response_url: None,
|
||||
})
|
||||
} else {
|
||||
let response = request.send_queue_request(&client.api_key).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: Some(response.request_id),
|
||||
gateway_request_id: None,
|
||||
status_url: Some(response.status_url),
|
||||
response_url: Some(response.response_url),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use fal_client::requests::api::image::edit::gpt_image_1_edit_image::api::{
|
||||
GptImage1EditImageNumImages, GptImage1EditImageQuality, GptImage1EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_1_text_to_image::api::{
|
||||
GptImage1TextToImageNumImages, GptImage1TextToImageQuality,
|
||||
GptImage1TextToImageSize,
|
||||
};
|
||||
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
|
||||
|
||||
fn client_without_webhook() -> RouterFalWebhookOptionalClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
RouterFalWebhookOptionalClient::new(FalApiKey::from_str(secret.trim()))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_text_to_image_queue() {
|
||||
let request = FalGptImage1RequestState::TextToImage(GptImage1TextToImageRequest {
|
||||
prompt: "a small ceramic robot reading by a window".to_string(),
|
||||
num_images: GptImage1TextToImageNumImages::One,
|
||||
image_size: Some(GptImage1TextToImageSize::Square),
|
||||
quality: Some(GptImage1TextToImageQuality::Low),
|
||||
background: None,
|
||||
output_format: None,
|
||||
});
|
||||
let response = request.send(&client_without_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_edit_image_queue() {
|
||||
let request = FalGptImage1RequestState::EditImage(GptImage1EditImageRequest {
|
||||
prompt: "make the image feel like a quiet watercolor study".to_string(),
|
||||
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
|
||||
num_images: GptImage1EditImageNumImages::One,
|
||||
mask_image_url: None,
|
||||
image_size: Some(GptImage1EditImageSize::Square),
|
||||
quality: Some(GptImage1EditImageQuality::Low),
|
||||
input_fidelity: None,
|
||||
background: None,
|
||||
output_format: None,
|
||||
});
|
||||
let response = request.send(&client_without_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some());
|
||||
}
|
||||
}
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
use fal_client::requests::api::image::edit::gpt_image_1p5_edit_image::api::{
|
||||
GptImage1p5EditImageNumImages, GptImage1p5EditImageQuality,
|
||||
GptImage1p5EditImageRequest, GptImage1p5EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_1p5_text_to_image::api::{
|
||||
GptImage1p5TextToImageNumImages, GptImage1p5TextToImageQuality,
|
||||
GptImage1p5TextToImageRequest, GptImage1p5TextToImageSize,
|
||||
};
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::request::FalGptImage1p5RequestState;
|
||||
|
||||
pub fn build_fal_gpt_image_1p5(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let prompt = builder.prompt.clone().unwrap_or_default();
|
||||
let image_urls = resolve_image_urls(builder.image_inputs.clone())?;
|
||||
let num_images = plan_num_images(builder.image_batch_count, builder.request_mismatch_mitigation_strategy)?;
|
||||
let image_size = plan_image_size(builder.aspect_ratio);
|
||||
let quality = plan_quality(builder.quality);
|
||||
|
||||
let state = if image_urls.is_empty() {
|
||||
FalGptImage1p5RequestState::TextToImage(GptImage1p5TextToImageRequest {
|
||||
prompt,
|
||||
num_images: to_t2i_num_images(num_images),
|
||||
image_size: image_size.map(to_t2i_image_size),
|
||||
background: None,
|
||||
quality: Some(to_t2i_quality(quality)),
|
||||
output_format: None,
|
||||
})
|
||||
} else {
|
||||
FalGptImage1p5RequestState::EditImage(GptImage1p5EditImageRequest {
|
||||
prompt,
|
||||
image_urls,
|
||||
num_images: to_edit_num_images(num_images),
|
||||
mask_image_url: None,
|
||||
image_size: image_size.map(to_edit_image_size),
|
||||
background: None,
|
||||
quality: Some(to_edit_quality(quality)),
|
||||
input_fidelity: None,
|
||||
output_format: None,
|
||||
})
|
||||
};
|
||||
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage1p5(state),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedQuality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedImageSize {
|
||||
Square,
|
||||
Wide,
|
||||
Tall,
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<PlannedNumImages, ArtcraftRouterError> {
|
||||
let count = count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(PlannedNumImages::One),
|
||||
2 => Ok(PlannedNumImages::Two),
|
||||
3 => Ok(PlannedNumImages::Three),
|
||||
4 => Ok(PlannedNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(PlannedNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_quality(quality: Option<CommonQuality>) -> PlannedQuality {
|
||||
match quality {
|
||||
Some(CommonQuality::Low) => PlannedQuality::Low,
|
||||
Some(CommonQuality::Medium) => PlannedQuality::Medium,
|
||||
Some(CommonQuality::High) | None => PlannedQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<PlannedImageSize> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => None,
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Some(PlannedImageSize::Square)
|
||||
}
|
||||
Some(CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(CommonAspectRatio::WideFourByThree)
|
||||
| Some(CommonAspectRatio::WideFiveByFour)
|
||||
| Some(CommonAspectRatio::WideSixteenByNine)
|
||||
| Some(CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(CommonAspectRatio::Wide) => Some(PlannedImageSize::Wide),
|
||||
Some(CommonAspectRatio::TallTwoByThree)
|
||||
| Some(CommonAspectRatio::TallThreeByFour)
|
||||
| Some(CommonAspectRatio::TallFourByFive)
|
||||
| Some(CommonAspectRatio::TallNineBySixteen)
|
||||
| Some(CommonAspectRatio::TallNineByTwentyOne)
|
||||
| Some(CommonAspectRatio::Tall) => Some(PlannedImageSize::Tall),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_num_images(n: PlannedNumImages) -> GptImage1p5TextToImageNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => GptImage1p5TextToImageNumImages::One,
|
||||
PlannedNumImages::Two => GptImage1p5TextToImageNumImages::Two,
|
||||
PlannedNumImages::Three => GptImage1p5TextToImageNumImages::Three,
|
||||
PlannedNumImages::Four => GptImage1p5TextToImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_num_images(n: PlannedNumImages) -> GptImage1p5EditImageNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => GptImage1p5EditImageNumImages::One,
|
||||
PlannedNumImages::Two => GptImage1p5EditImageNumImages::Two,
|
||||
PlannedNumImages::Three => GptImage1p5EditImageNumImages::Three,
|
||||
PlannedNumImages::Four => GptImage1p5EditImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_quality(q: PlannedQuality) -> GptImage1p5TextToImageQuality {
|
||||
match q {
|
||||
PlannedQuality::Low => GptImage1p5TextToImageQuality::Low,
|
||||
PlannedQuality::Medium => GptImage1p5TextToImageQuality::Medium,
|
||||
PlannedQuality::High => GptImage1p5TextToImageQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_quality(q: PlannedQuality) -> GptImage1p5EditImageQuality {
|
||||
match q {
|
||||
PlannedQuality::Low => GptImage1p5EditImageQuality::Low,
|
||||
PlannedQuality::Medium => GptImage1p5EditImageQuality::Medium,
|
||||
PlannedQuality::High => GptImage1p5EditImageQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_image_size(s: PlannedImageSize) -> GptImage1p5TextToImageSize {
|
||||
match s {
|
||||
PlannedImageSize::Square => GptImage1p5TextToImageSize::Square,
|
||||
PlannedImageSize::Wide => GptImage1p5TextToImageSize::Wide,
|
||||
PlannedImageSize::Tall => GptImage1p5TextToImageSize::Tall,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_image_size(s: PlannedImageSize) -> GptImage1p5EditImageSize {
|
||||
match s {
|
||||
PlannedImageSize::Square => GptImage1p5EditImageSize::Square,
|
||||
PlannedImageSize::Wide => GptImage1p5EditImageSize::Wide,
|
||||
PlannedImageSize::Tall => GptImage1p5EditImageSize::Tall,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_urls(
|
||||
image_inputs: Option<ImageListRef>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::GptImage1p5,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_t2i(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> GptImage1p5TextToImageRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage1p5(
|
||||
FalGptImage1p5RequestState::TextToImage(req)
|
||||
)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected GPT Image 1.5 text-to-image request")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
fn unwrap_edit(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> GptImage1p5EditImageRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage1p5(
|
||||
FalGptImage1p5RequestState::EditImage(req)
|
||||
)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected GPT Image 1.5 edit-image request")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
fn assert_debug<T: Debug>(actual: T, expected: &str) {
|
||||
assert_eq!(format!("{:?}", actual), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build2_routes_to_gpt_image_1p5_request() {
|
||||
let req = unwrap_t2i(base_builder().build2());
|
||||
assert_eq!(req.prompt, "a cat in space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_detection_is_based_on_image_urls() {
|
||||
let text_req = unwrap_t2i(build_fal_gpt_image_1p5(base_builder()));
|
||||
assert_eq!(text_req.prompt, "a cat in space");
|
||||
|
||||
let edit_req = unwrap_edit(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_eq!(edit_req.image_urls, vec!["https://example.com/img.jpg"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_file_tokens_are_rejected() {
|
||||
let result = build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(vec![])),
|
||||
..base_builder()
|
||||
});
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(1, "One"),
|
||||
(2, "Two"),
|
||||
(3, "Three"),
|
||||
(4, "Four"),
|
||||
];
|
||||
|
||||
for (count, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(count),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, expected);
|
||||
}
|
||||
|
||||
for (count, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
image_batch_count: Some(count),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_rejects_zero_and_handles_overflow_by_strategy() {
|
||||
assert!(matches!(
|
||||
build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(0),
|
||||
..base_builder()
|
||||
}),
|
||||
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
|
||||
));
|
||||
assert!(matches!(
|
||||
build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(5),
|
||||
..base_builder()
|
||||
}),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, "Four");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "Some(High)"),
|
||||
(Some(CommonQuality::Low), "Some(Low)"),
|
||||
(Some(CommonQuality::Medium), "Some(Medium)"),
|
||||
(Some(CommonQuality::High), "Some(High)"),
|
||||
];
|
||||
|
||||
for (quality, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
quality,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.quality, expected);
|
||||
}
|
||||
|
||||
for (quality, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
quality,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.quality, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "None"),
|
||||
(Some(CommonAspectRatio::Auto), "None"),
|
||||
(Some(CommonAspectRatio::Auto2k), "None"),
|
||||
(Some(CommonAspectRatio::Auto3k), "None"),
|
||||
(Some(CommonAspectRatio::Auto4k), "None"),
|
||||
(Some(CommonAspectRatio::Square), "Some(Square)"),
|
||||
(Some(CommonAspectRatio::SquareHd), "Some(Square)"),
|
||||
(Some(CommonAspectRatio::WideThreeByTwo), "Some(Wide)"),
|
||||
(Some(CommonAspectRatio::WideFourByThree), "Some(Wide)"),
|
||||
(Some(CommonAspectRatio::WideFiveByFour), "Some(Wide)"),
|
||||
(Some(CommonAspectRatio::WideSixteenByNine), "Some(Wide)"),
|
||||
(Some(CommonAspectRatio::WideTwentyOneByNine), "Some(Wide)"),
|
||||
(Some(CommonAspectRatio::Wide), "Some(Wide)"),
|
||||
(Some(CommonAspectRatio::TallTwoByThree), "Some(Tall)"),
|
||||
(Some(CommonAspectRatio::TallThreeByFour), "Some(Tall)"),
|
||||
(Some(CommonAspectRatio::TallFourByFive), "Some(Tall)"),
|
||||
(Some(CommonAspectRatio::TallNineBySixteen), "Some(Tall)"),
|
||||
(Some(CommonAspectRatio::TallNineByTwentyOne), "Some(Tall)"),
|
||||
(Some(CommonAspectRatio::Tall), "Some(Tall)"),
|
||||
];
|
||||
|
||||
for (aspect_ratio, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
aspect_ratio,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.image_size, expected);
|
||||
}
|
||||
|
||||
for (aspect_ratio, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
aspect_ratio,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.image_size, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::request::FalGptImage1p5RequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalGptImage1p5CostState {
|
||||
request: FalGptImage1p5RequestState,
|
||||
}
|
||||
|
||||
impl FalGptImage1p5CostState {
|
||||
pub fn from_request(request: &FalGptImage1p5RequestState) -> Self {
|
||||
Self { request: request.clone() }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = match &self.request {
|
||||
FalGptImage1p5RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage1p5RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
};
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use fal_client::requests::api::image::edit::gpt_image_1p5_edit_image::api::{GptImage1p5EditImageNumImages, GptImage1p5EditImageQuality, GptImage1p5EditImageRequest, GptImage1p5EditImageSize};
|
||||
use fal_client::requests::api::image::text::gpt_image_1p5_text_to_image::api::{GptImage1p5TextToImageNumImages, GptImage1p5TextToImageQuality, GptImage1p5TextToImageRequest, GptImage1p5TextToImageSize};
|
||||
|
||||
#[test]
|
||||
fn text_to_image_cost_table_covers_quality_size_and_count() {
|
||||
for (quality, image_size, expected_by_count) in text_to_image_price_cases() {
|
||||
for (num_images, expected) in text_to_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage1p5RequestState::TextToImage(GptImage1p5TextToImageRequest { prompt: "test".to_string(), num_images, image_size, background: None, quality, output_format: None });
|
||||
let cost = FalGptImage1p5CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_image_cost_table_covers_quality_size_and_count() {
|
||||
for (quality, image_size, expected_by_count) in edit_image_price_cases() {
|
||||
for (num_images, expected) in edit_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage1p5RequestState::EditImage(GptImage1p5EditImageRequest { prompt: "test".to_string(), image_urls: vec!["https://example.com/image.png".to_string()], num_images, mask_image_url: None, image_size, background: None, quality, input_fidelity: None, output_format: None });
|
||||
let cost = FalGptImage1p5CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn text_to_image_price_cases() -> [(Option<GptImage1p5TextToImageQuality>, Option<GptImage1p5TextToImageSize>, [u64; 4]); 10] {
|
||||
[(None, None, [3, 6, 9, 12]), (Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Square), [1, 2, 3, 4]), (Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Wide), [1, 2, 3, 4]), (Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Tall), [1, 2, 3, 4]), (Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Square), [3, 6, 9, 12]), (Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Wide), [5, 10, 15, 20]), (Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Tall), [5, 10, 15, 20]), (Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Square), [13, 26, 39, 52]), (Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Wide), [20, 40, 60, 80]), (Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Tall), [20, 40, 60, 80])]
|
||||
}
|
||||
|
||||
fn edit_image_price_cases() -> [(Option<GptImage1p5EditImageQuality>, Option<GptImage1p5EditImageSize>, [u64; 4]); 10] {
|
||||
[(None, None, [3, 6, 9, 12]), (Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Square), [1, 2, 3, 4]), (Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Wide), [1, 2, 3, 4]), (Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Tall), [1, 2, 3, 4]), (Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Square), [3, 6, 9, 12]), (Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Wide), [5, 10, 15, 20]), (Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Tall), [5, 10, 15, 20]), (Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Square), [13, 26, 39, 52]), (Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Wide), [20, 40, 60, 80]), (Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Tall), [20, 40, 60, 80])]
|
||||
}
|
||||
|
||||
fn text_to_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage1p5TextToImageNumImages, u64); 4] {
|
||||
[(GptImage1p5TextToImageNumImages::One, expected_by_count[0]), (GptImage1p5TextToImageNumImages::Two, expected_by_count[1]), (GptImage1p5TextToImageNumImages::Three, expected_by_count[2]), (GptImage1p5TextToImageNumImages::Four, expected_by_count[3])]
|
||||
}
|
||||
|
||||
fn edit_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage1p5EditImageNumImages, u64); 4] {
|
||||
[(GptImage1p5EditImageNumImages::One, expected_by_count[0]), (GptImage1p5EditImageNumImages::Two, expected_by_count[1]), (GptImage1p5EditImageNumImages::Three, expected_by_count[2]), (GptImage1p5EditImageNumImages::Four, expected_by_count[3])]
|
||||
}
|
||||
|
||||
fn assert_standard_cost_estimate(cost: ImageGenerationCostEstimate, expected_usd_cents: u64) {
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(expected_usd_cents));
|
||||
assert_eq!(cost.cost_in_credits, None);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::api::image::edit::gpt_image_1p5_edit_image::api::GptImage1p5EditImageRequest;
|
||||
use fal_client::requests::api::image::text::gpt_image_1p5_text_to_image::api::GptImage1p5TextToImageRequest;
|
||||
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::{FalImageResponsePayload, GenerateImageResponse};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FalGptImage1p5RequestState {
|
||||
TextToImage(GptImage1p5TextToImageRequest),
|
||||
EditImage(GptImage1p5EditImageRequest),
|
||||
}
|
||||
|
||||
impl FalGptImage1p5RequestState {
|
||||
pub async fn send(&self, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
Self::TextToImage(request) => send_request(request, client).await,
|
||||
Self::EditImage(request) => send_request(request, client).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FalResponseIds {
|
||||
request_id: Option<String>,
|
||||
gateway_request_id: Option<String>,
|
||||
status_url: Option<String>,
|
||||
response_url: Option<String>,
|
||||
}
|
||||
|
||||
async fn send_request<T>(request: &T, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError>
|
||||
where
|
||||
T: FalEndpoint + Clone + Debug + Send + Sync + 'static,
|
||||
{
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
|
||||
let payload = send_fal_request(request, client).await?;
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: payload.request_id,
|
||||
gateway_request_id: payload.gateway_request_id,
|
||||
maybe_status_url: payload.status_url,
|
||||
maybe_response_url: payload.response_url,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn send_fal_request<T: FalEndpoint>(
|
||||
request: &T,
|
||||
client: &RouterFalWebhookOptionalClient,
|
||||
) -> Result<FalResponseIds, ArtcraftRouterError> {
|
||||
if let Some(webhook_url) = &client.webhook_url {
|
||||
let response = request.send_webhook_request(&client.api_key, webhook_url).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: response.request_id,
|
||||
gateway_request_id: response.gateway_request_id,
|
||||
status_url: None,
|
||||
response_url: None,
|
||||
})
|
||||
} else {
|
||||
let response = request.send_queue_request(&client.api_key).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: Some(response.request_id),
|
||||
gateway_request_id: None,
|
||||
status_url: Some(response.status_url),
|
||||
response_url: Some(response.response_url),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use fal_client::requests::api::image::edit::gpt_image_1p5_edit_image::api::{
|
||||
GptImage1p5EditImageNumImages, GptImage1p5EditImageQuality,
|
||||
GptImage1p5EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_1p5_text_to_image::api::{
|
||||
GptImage1p5TextToImageNumImages, GptImage1p5TextToImageQuality,
|
||||
GptImage1p5TextToImageSize,
|
||||
};
|
||||
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
|
||||
|
||||
fn client_without_webhook() -> RouterFalWebhookOptionalClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
RouterFalWebhookOptionalClient::new(FalApiKey::from_str(secret.trim()))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_text_to_image_queue() {
|
||||
let request = FalGptImage1p5RequestState::TextToImage(GptImage1p5TextToImageRequest {
|
||||
prompt: "a tiny brass observatory in a snowy field".to_string(),
|
||||
num_images: GptImage1p5TextToImageNumImages::One,
|
||||
image_size: Some(GptImage1p5TextToImageSize::Square),
|
||||
background: None,
|
||||
quality: Some(GptImage1p5TextToImageQuality::Low),
|
||||
output_format: None,
|
||||
});
|
||||
let response = request.send(&client_without_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_edit_image_queue() {
|
||||
let request = FalGptImage1p5RequestState::EditImage(GptImage1p5EditImageRequest {
|
||||
prompt: "turn this into a softly lit editorial photograph".to_string(),
|
||||
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
|
||||
num_images: GptImage1p5EditImageNumImages::One,
|
||||
mask_image_url: None,
|
||||
image_size: Some(GptImage1p5EditImageSize::Square),
|
||||
background: None,
|
||||
quality: Some(GptImage1p5EditImageQuality::Low),
|
||||
input_fidelity: None,
|
||||
output_format: None,
|
||||
});
|
||||
let response = request.send(&client_without_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some());
|
||||
}
|
||||
}
|
||||
+458
@@ -0,0 +1,458 @@
|
||||
use fal_client::requests::api::image::common::gpt_image_2_resolution::GptImage2Resolution;
|
||||
use fal_client::requests::api::image::edit::gpt_image_2_edit_image::api::{
|
||||
GptImage2EditImageNumImages, GptImage2EditImageQuality, GptImage2EditImageRequest,
|
||||
GptImage2EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_2_text_to_image::api::{
|
||||
GptImage2TextToImageNumImages, GptImage2TextToImageQuality,
|
||||
GptImage2TextToImageRequest, GptImage2TextToImageSize,
|
||||
};
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::request::FalGptImage2RequestState;
|
||||
|
||||
pub fn build_fal_gpt_image_2(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let prompt = builder.prompt.clone().unwrap_or_default();
|
||||
let image_urls = resolve_image_urls(builder.image_inputs.clone())?;
|
||||
let num_images = plan_num_images(builder.image_batch_count, builder.request_mismatch_mitigation_strategy)?;
|
||||
let image_size = plan_image_size(builder.aspect_ratio);
|
||||
let resolution = plan_resolution(builder.resolution);
|
||||
let quality = plan_quality(builder.quality);
|
||||
|
||||
let state = if image_urls.is_empty() {
|
||||
FalGptImage2RequestState::TextToImage(GptImage2TextToImageRequest {
|
||||
prompt,
|
||||
num_images: to_t2i_num_images(num_images),
|
||||
image_size: image_size.and_then(to_t2i_image_size),
|
||||
resolution,
|
||||
quality: Some(to_t2i_quality(quality)),
|
||||
output_format: None,
|
||||
})
|
||||
} else {
|
||||
FalGptImage2RequestState::EditImage(GptImage2EditImageRequest {
|
||||
prompt,
|
||||
image_urls,
|
||||
num_images: to_edit_num_images(num_images),
|
||||
mask_url: None,
|
||||
image_size: image_size.map(to_edit_image_size),
|
||||
resolution,
|
||||
quality: Some(to_edit_quality(quality)),
|
||||
output_format: None,
|
||||
})
|
||||
};
|
||||
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage2(state),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedQuality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedImageSize {
|
||||
SquareHd,
|
||||
Square,
|
||||
Portrait4x3,
|
||||
Portrait16x9,
|
||||
Landscape4x3,
|
||||
Landscape16x9,
|
||||
Auto,
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<PlannedNumImages, ArtcraftRouterError> {
|
||||
let count = count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(PlannedNumImages::One),
|
||||
2 => Ok(PlannedNumImages::Two),
|
||||
3 => Ok(PlannedNumImages::Three),
|
||||
4 => Ok(PlannedNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(PlannedNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_quality(quality: Option<CommonQuality>) -> PlannedQuality {
|
||||
match quality {
|
||||
Some(CommonQuality::Low) => PlannedQuality::Low,
|
||||
Some(CommonQuality::Medium) => PlannedQuality::Medium,
|
||||
Some(CommonQuality::High) | None => PlannedQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(resolution: Option<CommonResolution>) -> Option<GptImage2Resolution> {
|
||||
resolution.map(|r| match r {
|
||||
CommonResolution::HalfK
|
||||
| CommonResolution::FourEightyP
|
||||
| CommonResolution::SevenTwentyP
|
||||
| CommonResolution::OneK => GptImage2Resolution::OneK,
|
||||
CommonResolution::TenEightyP | CommonResolution::TwoK => GptImage2Resolution::TwoK,
|
||||
CommonResolution::ThreeK => GptImage2Resolution::ThreeK,
|
||||
CommonResolution::FourK => GptImage2Resolution::FourK,
|
||||
})
|
||||
}
|
||||
|
||||
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<PlannedImageSize> {
|
||||
match aspect_ratio {
|
||||
None => None,
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Some(PlannedImageSize::Auto),
|
||||
Some(CommonAspectRatio::Square) => Some(PlannedImageSize::Square),
|
||||
Some(CommonAspectRatio::SquareHd) => Some(PlannedImageSize::SquareHd),
|
||||
Some(CommonAspectRatio::WideFourByThree)
|
||||
| Some(CommonAspectRatio::WideFiveByFour) => Some(PlannedImageSize::Landscape4x3),
|
||||
Some(CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(CommonAspectRatio::WideSixteenByNine)
|
||||
| Some(CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(CommonAspectRatio::Wide) => Some(PlannedImageSize::Landscape16x9),
|
||||
Some(CommonAspectRatio::TallThreeByFour)
|
||||
| Some(CommonAspectRatio::TallFourByFive) => Some(PlannedImageSize::Portrait4x3),
|
||||
Some(CommonAspectRatio::TallTwoByThree)
|
||||
| Some(CommonAspectRatio::TallNineBySixteen)
|
||||
| Some(CommonAspectRatio::TallNineByTwentyOne)
|
||||
| Some(CommonAspectRatio::Tall) => Some(PlannedImageSize::Portrait16x9),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_num_images(n: PlannedNumImages) -> GptImage2TextToImageNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => GptImage2TextToImageNumImages::One,
|
||||
PlannedNumImages::Two => GptImage2TextToImageNumImages::Two,
|
||||
PlannedNumImages::Three => GptImage2TextToImageNumImages::Three,
|
||||
PlannedNumImages::Four => GptImage2TextToImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_num_images(n: PlannedNumImages) -> GptImage2EditImageNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => GptImage2EditImageNumImages::One,
|
||||
PlannedNumImages::Two => GptImage2EditImageNumImages::Two,
|
||||
PlannedNumImages::Three => GptImage2EditImageNumImages::Three,
|
||||
PlannedNumImages::Four => GptImage2EditImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_quality(q: PlannedQuality) -> GptImage2TextToImageQuality {
|
||||
match q {
|
||||
PlannedQuality::Low => GptImage2TextToImageQuality::Low,
|
||||
PlannedQuality::Medium => GptImage2TextToImageQuality::Medium,
|
||||
PlannedQuality::High => GptImage2TextToImageQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_quality(q: PlannedQuality) -> GptImage2EditImageQuality {
|
||||
match q {
|
||||
PlannedQuality::Low => GptImage2EditImageQuality::Low,
|
||||
PlannedQuality::Medium => GptImage2EditImageQuality::Medium,
|
||||
PlannedQuality::High => GptImage2EditImageQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_image_size(s: PlannedImageSize) -> Option<GptImage2TextToImageSize> {
|
||||
match s {
|
||||
PlannedImageSize::SquareHd => Some(GptImage2TextToImageSize::SquareHd),
|
||||
PlannedImageSize::Square => Some(GptImage2TextToImageSize::Square),
|
||||
PlannedImageSize::Portrait4x3 => Some(GptImage2TextToImageSize::Portrait4x3),
|
||||
PlannedImageSize::Portrait16x9 => Some(GptImage2TextToImageSize::Portrait16x9),
|
||||
PlannedImageSize::Landscape4x3 => Some(GptImage2TextToImageSize::Landscape4x3),
|
||||
PlannedImageSize::Landscape16x9 => Some(GptImage2TextToImageSize::Landscape16x9),
|
||||
PlannedImageSize::Auto => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_image_size(s: PlannedImageSize) -> GptImage2EditImageSize {
|
||||
match s {
|
||||
PlannedImageSize::SquareHd => GptImage2EditImageSize::SquareHd,
|
||||
PlannedImageSize::Square => GptImage2EditImageSize::Square,
|
||||
PlannedImageSize::Portrait4x3 => GptImage2EditImageSize::Portrait4x3,
|
||||
PlannedImageSize::Portrait16x9 => GptImage2EditImageSize::Portrait16x9,
|
||||
PlannedImageSize::Landscape4x3 => GptImage2EditImageSize::Landscape4x3,
|
||||
PlannedImageSize::Landscape16x9 => GptImage2EditImageSize::Landscape16x9,
|
||||
PlannedImageSize::Auto => GptImage2EditImageSize::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_urls(
|
||||
image_inputs: Option<ImageListRef>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::GptImage2,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_t2i(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> GptImage2TextToImageRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage2(
|
||||
FalGptImage2RequestState::TextToImage(req)
|
||||
)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected GPT Image 2 text-to-image request")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
fn unwrap_edit(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> GptImage2EditImageRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage2(
|
||||
FalGptImage2RequestState::EditImage(req)
|
||||
)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected GPT Image 2 edit-image request")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
fn assert_debug<T: Debug>(actual: T, expected: &str) {
|
||||
assert_eq!(format!("{:?}", actual), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build2_routes_to_gpt_image_2_request() {
|
||||
let req = unwrap_t2i(base_builder().build2());
|
||||
assert_eq!(req.prompt, "a cat in space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_detection_is_based_on_image_urls() {
|
||||
let text_req = unwrap_t2i(build_fal_gpt_image_2(base_builder()));
|
||||
assert_eq!(text_req.prompt, "a cat in space");
|
||||
|
||||
let edit_req = unwrap_edit(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_eq!(edit_req.image_urls, vec!["https://example.com/img.jpg"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_file_tokens_are_rejected() {
|
||||
let result = build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(vec![])),
|
||||
..base_builder()
|
||||
});
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(1, "One"),
|
||||
(2, "Two"),
|
||||
(3, "Three"),
|
||||
(4, "Four"),
|
||||
];
|
||||
|
||||
for (count, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(count),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, expected);
|
||||
}
|
||||
|
||||
for (count, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
image_batch_count: Some(count),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_rejects_zero_and_handles_overflow_by_strategy() {
|
||||
assert!(matches!(
|
||||
build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(0),
|
||||
..base_builder()
|
||||
}),
|
||||
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
|
||||
));
|
||||
assert!(matches!(
|
||||
build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(5),
|
||||
..base_builder()
|
||||
}),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
|
||||
let req = unwrap_t2i(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, "Four");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "Some(High)"),
|
||||
(Some(CommonQuality::Low), "Some(Low)"),
|
||||
(Some(CommonQuality::Medium), "Some(Medium)"),
|
||||
(Some(CommonQuality::High), "Some(High)"),
|
||||
];
|
||||
|
||||
for (quality, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
quality,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.quality, expected);
|
||||
}
|
||||
|
||||
for (quality, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
quality,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.quality, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "None"),
|
||||
(Some(CommonResolution::HalfK), "Some(OneK)"),
|
||||
(Some(CommonResolution::FourEightyP), "Some(OneK)"),
|
||||
(Some(CommonResolution::SevenTwentyP), "Some(OneK)"),
|
||||
(Some(CommonResolution::OneK), "Some(OneK)"),
|
||||
(Some(CommonResolution::TenEightyP), "Some(TwoK)"),
|
||||
(Some(CommonResolution::TwoK), "Some(TwoK)"),
|
||||
(Some(CommonResolution::ThreeK), "Some(ThreeK)"),
|
||||
(Some(CommonResolution::FourK), "Some(FourK)"),
|
||||
];
|
||||
|
||||
for (resolution, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
resolution,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.resolution, expected);
|
||||
}
|
||||
|
||||
for (resolution, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
resolution,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.resolution, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "None", "None"),
|
||||
(Some(CommonAspectRatio::Auto), "None", "Some(Auto)"),
|
||||
(Some(CommonAspectRatio::Auto2k), "None", "Some(Auto)"),
|
||||
(Some(CommonAspectRatio::Auto3k), "None", "Some(Auto)"),
|
||||
(Some(CommonAspectRatio::Auto4k), "None", "Some(Auto)"),
|
||||
(Some(CommonAspectRatio::Square), "Some(Square)", "Some(Square)"),
|
||||
(Some(CommonAspectRatio::SquareHd), "Some(SquareHd)", "Some(SquareHd)"),
|
||||
(Some(CommonAspectRatio::WideThreeByTwo), "Some(Landscape16x9)", "Some(Landscape16x9)"),
|
||||
(Some(CommonAspectRatio::WideFourByThree), "Some(Landscape4x3)", "Some(Landscape4x3)"),
|
||||
(Some(CommonAspectRatio::WideFiveByFour), "Some(Landscape4x3)", "Some(Landscape4x3)"),
|
||||
(Some(CommonAspectRatio::WideSixteenByNine), "Some(Landscape16x9)", "Some(Landscape16x9)"),
|
||||
(Some(CommonAspectRatio::WideTwentyOneByNine), "Some(Landscape16x9)", "Some(Landscape16x9)"),
|
||||
(Some(CommonAspectRatio::Wide), "Some(Landscape16x9)", "Some(Landscape16x9)"),
|
||||
(Some(CommonAspectRatio::TallTwoByThree), "Some(Portrait16x9)", "Some(Portrait16x9)"),
|
||||
(Some(CommonAspectRatio::TallThreeByFour), "Some(Portrait4x3)", "Some(Portrait4x3)"),
|
||||
(Some(CommonAspectRatio::TallFourByFive), "Some(Portrait4x3)", "Some(Portrait4x3)"),
|
||||
(Some(CommonAspectRatio::TallNineBySixteen), "Some(Portrait16x9)", "Some(Portrait16x9)"),
|
||||
(Some(CommonAspectRatio::TallNineByTwentyOne), "Some(Portrait16x9)", "Some(Portrait16x9)"),
|
||||
(Some(CommonAspectRatio::Tall), "Some(Portrait16x9)", "Some(Portrait16x9)"),
|
||||
];
|
||||
|
||||
for (aspect_ratio, expected_t2i, _) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
aspect_ratio,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.image_size, expected_t2i);
|
||||
}
|
||||
|
||||
for (aspect_ratio, _, expected_edit) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
aspect_ratio,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.image_size, expected_edit);
|
||||
}
|
||||
}
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::request::FalGptImage2RequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalGptImage2CostState {
|
||||
request: FalGptImage2RequestState,
|
||||
}
|
||||
|
||||
impl FalGptImage2CostState {
|
||||
pub fn from_request(request: &FalGptImage2RequestState) -> Self {
|
||||
Self { request: request.clone() }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = match &self.request {
|
||||
FalGptImage2RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage2RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
};
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use fal_client::requests::api::image::common::gpt_image_2_resolution::GptImage2Resolution;
|
||||
use fal_client::requests::api::image::edit::gpt_image_2_edit_image::api::{GptImage2EditImageNumImages, GptImage2EditImageQuality, GptImage2EditImageRequest, GptImage2EditImageSize};
|
||||
use fal_client::requests::api::image::text::gpt_image_2_text_to_image::api::{GptImage2TextToImageNumImages, GptImage2TextToImageQuality, GptImage2TextToImageRequest, GptImage2TextToImageSize};
|
||||
|
||||
type TextPriceCase = (Option<GptImage2TextToImageQuality>, Option<GptImage2TextToImageSize>, Option<GptImage2Resolution>, [u64; 4]);
|
||||
type EditPriceCase = (Option<GptImage2EditImageQuality>, Option<GptImage2EditImageSize>, Option<GptImage2Resolution>, [u64; 4]);
|
||||
|
||||
#[test]
|
||||
fn text_to_image_cost_table_covers_quality_size_resolution_and_count() {
|
||||
for (quality, image_size, resolution, expected_by_count) in text_to_image_price_cases() {
|
||||
for (num_images, expected) in text_to_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage2RequestState::TextToImage(GptImage2TextToImageRequest { prompt: "test".to_string(), num_images, image_size, resolution, quality, output_format: None });
|
||||
let cost = FalGptImage2CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_image_cost_table_covers_quality_size_resolution_and_count() {
|
||||
for (quality, image_size, resolution, expected_by_count) in edit_image_price_cases() {
|
||||
for (num_images, expected) in edit_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage2RequestState::EditImage(GptImage2EditImageRequest { prompt: "test".to_string(), image_urls: vec!["https://example.com/image.png".to_string()], num_images, mask_url: None, image_size, resolution, quality, output_format: None });
|
||||
let cost = FalGptImage2CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn text_to_image_price_cases() -> Vec<TextPriceCase> {
|
||||
let mut cases = vec![
|
||||
// Default: High quality, Square
|
||||
(None, None, None, [16, 32, 48, 64]),
|
||||
// Presets (no resolution)
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Square), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape16x9), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait16x9), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::SquareHd), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape4x3), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait4x3), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Square), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape16x9), None, [6, 12, 18, 24]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait16x9), None, [6, 12, 18, 24]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::SquareHd), None, [8, 16, 24, 32]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape4x3), None, [15, 30, 45, 60]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait4x3), None, [15, 30, 45, 60]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Square), None, [16, 32, 48, 64]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape16x9), None, [20, 40, 60, 80]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait16x9), None, [20, 40, 60, 80]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd), None, [27, 54, 81, 108]),
|
||||
];
|
||||
|
||||
for (size, resolution, low, medium, high) in text_to_image_resolution_price_cases() {
|
||||
cases.push((Some(GptImage2TextToImageQuality::Low), Some(size), Some(resolution), low));
|
||||
cases.push((Some(GptImage2TextToImageQuality::Medium), Some(size), Some(resolution), medium));
|
||||
cases.push((Some(GptImage2TextToImageQuality::High), Some(size), Some(resolution), high));
|
||||
}
|
||||
|
||||
cases
|
||||
}
|
||||
|
||||
fn edit_image_price_cases() -> Vec<EditPriceCase> {
|
||||
let mut cases = vec![
|
||||
// Default: High quality, Square
|
||||
(None, None, None, [15, 30, 45, 60]),
|
||||
// Auto (with and without resolution — same cost, conservative max)
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), None, [10, 20, 30, 40]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), None, [41, 82, 123, 164]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [2, 4, 6, 8]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [10, 20, 30, 40]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [41, 82, 123, 164]),
|
||||
// Presets (no resolution)
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Square), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape16x9), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait16x9), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::SquareHd), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3), None, [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait4x3), None, [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Square), None, [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape16x9), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait16x9), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::SquareHd), None, [7, 14, 21, 28]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape4x3), None, [14, 28, 42, 56]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait4x3), None, [14, 28, 42, 56]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Square), None, [15, 30, 45, 60]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape16x9), None, [19, 38, 57, 76]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait16x9), None, [19, 38, 57, 76]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd), None, [26, 52, 78, 104]),
|
||||
];
|
||||
|
||||
for (size, resolution, low, medium, high) in edit_image_resolution_price_cases() {
|
||||
cases.push((Some(GptImage2EditImageQuality::Low), Some(size), Some(resolution), low));
|
||||
cases.push((Some(GptImage2EditImageQuality::Medium), Some(size), Some(resolution), medium));
|
||||
cases.push((Some(GptImage2EditImageQuality::High), Some(size), Some(resolution), high));
|
||||
}
|
||||
|
||||
cases
|
||||
}
|
||||
|
||||
fn text_to_image_resolution_price_cases() -> Vec<(GptImage2TextToImageSize, GptImage2Resolution, [u64; 4], [u64; 4], [u64; 4])> {
|
||||
vec![
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 48, 72, 96]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [11, 22, 33, 44], [37, 74, 111, 148]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [41, 82, 123, 164]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::TwoK, [2, 4, 6, 8], [6, 12, 18, 24], [21, 42, 63, 84]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [31, 62, 93, 124]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 48, 72, 96]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [11, 22, 33, 44], [37, 74, 111, 148]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [41, 82, 123, 164]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::TwoK, [2, 4, 6, 8], [6, 12, 18, 24], [21, 42, 63, 84]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [31, 62, 93, 124]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [16, 32, 48, 64]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::TwoK, [2, 4, 6, 8], [8, 16, 24, 32], [27, 54, 81, 108]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::ThreeK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [16, 32, 48, 64]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::TwoK, [2, 4, 6, 8], [8, 16, 24, 32], [27, 54, 81, 108]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::ThreeK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
]
|
||||
}
|
||||
|
||||
fn edit_image_resolution_price_cases() -> Vec<(GptImage2EditImageSize, GptImage2Resolution, [u64; 4], [u64; 4], [u64; 4])> {
|
||||
vec![
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::TwoK, [1, 2, 3, 4], [6, 12, 18, 24], [23, 46, 69, 92]),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [36, 72, 108, 144]),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [40, 80, 120, 160]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::TwoK, [1, 2, 3, 4], [5, 10, 15, 20], [20, 40, 60, 80]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::ThreeK, [1, 2, 3, 4], [8, 16, 24, 32], [30, 60, 90, 120]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::TwoK, [1, 2, 3, 4], [6, 12, 18, 24], [23, 46, 69, 92]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [36, 72, 108, 144]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [40, 80, 120, 160]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::TwoK, [1, 2, 3, 4], [5, 10, 15, 20], [20, 40, 60, 80]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::ThreeK, [1, 2, 3, 4], [8, 16, 24, 32], [30, 60, 90, 120]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [15, 30, 45, 60]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::TwoK, [1, 2, 3, 4], [7, 14, 21, 28], [26, 52, 78, 104]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [15, 30, 45, 60]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::TwoK, [1, 2, 3, 4], [7, 14, 21, 28], [26, 52, 78, 104]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
]
|
||||
}
|
||||
|
||||
fn text_to_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage2TextToImageNumImages, u64); 4] {
|
||||
[(GptImage2TextToImageNumImages::One, expected_by_count[0]), (GptImage2TextToImageNumImages::Two, expected_by_count[1]), (GptImage2TextToImageNumImages::Three, expected_by_count[2]), (GptImage2TextToImageNumImages::Four, expected_by_count[3])]
|
||||
}
|
||||
|
||||
fn edit_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage2EditImageNumImages, u64); 4] {
|
||||
[(GptImage2EditImageNumImages::One, expected_by_count[0]), (GptImage2EditImageNumImages::Two, expected_by_count[1]), (GptImage2EditImageNumImages::Three, expected_by_count[2]), (GptImage2EditImageNumImages::Four, expected_by_count[3])]
|
||||
}
|
||||
|
||||
fn assert_standard_cost_estimate(cost: ImageGenerationCostEstimate, expected_usd_cents: u64) {
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(expected_usd_cents));
|
||||
assert_eq!(cost.cost_in_credits, None);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::api::image::edit::gpt_image_2_edit_image::api::GptImage2EditImageRequest;
|
||||
use fal_client::requests::api::image::text::gpt_image_2_text_to_image::api::GptImage2TextToImageRequest;
|
||||
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::{FalImageResponsePayload, GenerateImageResponse};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FalGptImage2RequestState {
|
||||
TextToImage(GptImage2TextToImageRequest),
|
||||
EditImage(GptImage2EditImageRequest),
|
||||
}
|
||||
|
||||
impl FalGptImage2RequestState {
|
||||
pub async fn send(&self, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
Self::TextToImage(request) => send_request(request, client).await,
|
||||
Self::EditImage(request) => send_request(request, client).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FalResponseIds {
|
||||
request_id: Option<String>,
|
||||
gateway_request_id: Option<String>,
|
||||
status_url: Option<String>,
|
||||
response_url: Option<String>,
|
||||
}
|
||||
|
||||
async fn send_request<T>(request: &T, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError>
|
||||
where
|
||||
T: FalEndpoint + Clone + Debug + Send + Sync + 'static,
|
||||
{
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
|
||||
let payload = send_fal_request(request, client).await?;
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: payload.request_id,
|
||||
gateway_request_id: payload.gateway_request_id,
|
||||
maybe_status_url: payload.status_url,
|
||||
maybe_response_url: payload.response_url,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn send_fal_request<T: FalEndpoint>(
|
||||
request: &T,
|
||||
client: &RouterFalWebhookOptionalClient,
|
||||
) -> Result<FalResponseIds, ArtcraftRouterError> {
|
||||
if let Some(webhook_url) = &client.webhook_url {
|
||||
let response = request.send_webhook_request(&client.api_key, webhook_url).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: response.request_id,
|
||||
gateway_request_id: response.gateway_request_id,
|
||||
status_url: None,
|
||||
response_url: None,
|
||||
})
|
||||
} else {
|
||||
let response = request.send_queue_request(&client.api_key).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: Some(response.request_id),
|
||||
gateway_request_id: None,
|
||||
status_url: Some(response.status_url),
|
||||
response_url: Some(response.response_url),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use fal_client::requests::api::image::common::gpt_image_2_resolution::GptImage2Resolution;
|
||||
use fal_client::requests::api::image::edit::gpt_image_2_edit_image::api::{
|
||||
GptImage2EditImageNumImages, GptImage2EditImageQuality, GptImage2EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_2_text_to_image::api::{
|
||||
GptImage2TextToImageNumImages, GptImage2TextToImageQuality,
|
||||
GptImage2TextToImageSize,
|
||||
};
|
||||
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
|
||||
|
||||
fn client_without_webhook() -> RouterFalWebhookOptionalClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
RouterFalWebhookOptionalClient::new(FalApiKey::from_str(secret.trim()))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_text_to_image_queue() {
|
||||
let request = FalGptImage2RequestState::TextToImage(GptImage2TextToImageRequest {
|
||||
prompt: "a polished product photo of a glass compass".to_string(),
|
||||
num_images: GptImage2TextToImageNumImages::One,
|
||||
image_size: Some(GptImage2TextToImageSize::Square),
|
||||
resolution: Some(GptImage2Resolution::OneK),
|
||||
quality: Some(GptImage2TextToImageQuality::Low),
|
||||
output_format: None,
|
||||
});
|
||||
let response = request.send(&client_without_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_edit_image_queue() {
|
||||
let request = FalGptImage2RequestState::EditImage(GptImage2EditImageRequest {
|
||||
prompt: "make this look like an elegant magazine cover image".to_string(),
|
||||
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
|
||||
num_images: GptImage2EditImageNumImages::One,
|
||||
mask_url: None,
|
||||
image_size: Some(GptImage2EditImageSize::Square),
|
||||
resolution: Some(GptImage2Resolution::OneK),
|
||||
quality: Some(GptImage2EditImageQuality::Low),
|
||||
output_format: None,
|
||||
});
|
||||
let response = request.send(&client_without_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some());
|
||||
}
|
||||
}
|
||||
+3
@@ -1,4 +1,7 @@
|
||||
pub mod flux_1_dev;
|
||||
pub mod flux_1_schnell;
|
||||
pub mod gpt_image_1;
|
||||
pub mod gpt_image_1p5;
|
||||
pub mod gpt_image_2;
|
||||
pub mod nano_banana_2;
|
||||
pub mod nano_banana_pro;
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ impl FalNanoBanana2CostState {
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ impl FalNanoBananaProCostState {
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ pub(crate) fn estimate_splat_cost_artcraft_marble_0p1_mini(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ pub(crate) fn estimate_splat_cost_artcraft_marble_0p1_plus(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -6,4 +6,5 @@ pub struct SplatGenerationCostEstimate {
|
||||
pub is_unlimited: bool,
|
||||
pub is_rate_limited: bool,
|
||||
pub has_watermark: bool,
|
||||
pub failures_are_refunded: Option<bool>,
|
||||
}
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ pub(crate) fn estimate_video_cost_artcraft_kling3p0_pro(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ pub(crate) fn estimate_video_cost_artcraft_kling3p0_standard(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -14,5 +14,6 @@ pub(crate) fn estimate_video_cost_artcraft_kling_1_6_pro(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -14,5 +14,6 @@ pub(crate) fn estimate_video_cost_artcraft_kling_2_1_master(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -14,5 +14,6 @@ pub(crate) fn estimate_video_cost_artcraft_kling_2_1_pro(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -14,5 +14,6 @@ pub(crate) fn estimate_video_cost_artcraft_kling_2_5_turbo_pro(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -22,5 +22,6 @@ pub(crate) fn estimate_video_cost_artcraft_kling_2_6_pro(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -50,6 +50,7 @@ pub(crate) fn estimate_video_cost_artcraft_seedance1p5_pro(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ pub (crate) fn estimate_video_cost_artcraft_seedance2p0(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ pub(crate) fn estimate_video_cost_artcraft_seedance2p0_fast(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -42,5 +42,6 @@ pub(crate) fn estimate_video_cost_artcraft_seedance_1_0_lite(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -14,5 +14,6 @@ pub(crate) fn estimate_video_cost_artcraft_sora_2(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -17,5 +17,6 @@ pub(crate) fn estimate_video_cost_artcraft_sora_2_pro(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -16,5 +16,6 @@ pub(crate) fn estimate_video_cost_artcraft_veo_2(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -17,5 +17,6 @@ pub(crate) fn estimate_video_cost_artcraft_veo_3(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -17,5 +17,6 @@ pub(crate) fn estimate_video_cost_artcraft_veo_3_fast(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -17,5 +17,6 @@ pub(crate) fn estimate_video_cost_artcraft_veo_3p1(
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user