mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-24 10:39:28 -04:00
Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a3693a7ce | ||
|
|
e3c99add5d | ||
|
|
8c3f880655 | ||
|
|
08300cb47c | ||
|
|
0dc0a2f597 | ||
|
|
f91f1965a4 | ||
|
|
cc1ed0eb76 | ||
|
|
93e9d423cb | ||
|
|
e6267eeb23 | ||
|
|
4e28d05e51 | ||
|
|
0d67b81f5f | ||
|
|
24addb0a49 | ||
|
|
041cfc5460 | ||
|
|
693b8c36be | ||
|
|
8c2e37cad7 | ||
|
|
5a30046efa | ||
|
|
2d9773100e | ||
|
|
1ad16c1d87 | ||
|
|
a531448611 | ||
|
|
dba5b095c4 | ||
|
|
d4323da8de | ||
|
|
7eb2a3cdba | ||
|
|
708930dfb4 | ||
|
|
4f0916edb7 | ||
|
|
c36e88d8ce | ||
|
|
233f60acf6 | ||
|
|
a3e16e491a | ||
|
|
3d521fec79 | ||
|
|
6c8979123f | ||
|
|
8d4edcfafd | ||
|
|
b53e138dab | ||
|
|
dc1b038ffa | ||
|
|
418a9f92c0 | ||
|
|
d671f7f951 | ||
|
|
e97e5c239e | ||
|
|
a7270f4685 | ||
|
|
a6fb661fbe | ||
|
|
e5f5a06b6b | ||
|
|
48a6b1a046 | ||
|
|
7791b9b6cc | ||
|
|
ff30213af8 | ||
|
|
766f726dc1 | ||
|
|
76d7d4c100 | ||
|
|
2902db074e | ||
|
|
b8ee03f1b7 | ||
|
|
2cf300588e | ||
|
|
d2720d0a62 | ||
|
|
69ed3ec48b | ||
|
|
81f5797e2f | ||
|
|
a80c69ab1e |
@@ -26,14 +26,23 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: install wreq dependencies
|
||||
run: |
|
||||
choco install cmake -y
|
||||
choco install strawberryperl -y
|
||||
choco install pkgconfiglite -y
|
||||
choco install llvm -y
|
||||
choco install nasm -y
|
||||
shell: cmd
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: install frontend dependencies
|
||||
run: |
|
||||
# Navigate to the frontend directory to install dependencies
|
||||
@@ -83,6 +92,8 @@ jobs:
|
||||
- name: build and publish
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
# NB(bt): This is needed by wreq. Hopefully it doesn't kill Tauri
|
||||
RUSTFLAGS: "-C target-feature=+crt-static"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Use the SQLx query cache for SQLite
|
||||
SQLX_OFFLINE: true
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = 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_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
|
||||
"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 = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 13
|
||||
"Right": 14
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0fa824822cd94024dce21243f41e69e4a3e8685c4de676834a7c0ee14982c976"
|
||||
"hash": "002cdb3d086f3b4181e2997befa54bd8516cb1d3d3bb6e389862695318d5c299"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO tasks (\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 9
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "01a0bf33e8331c19dc73f064c54eaa863bec158ef7713450a06c75e246f5f08c"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO prompts\nSET\n token = ?,\n prompt_type = ?,\n\n maybe_creator_user_token = ?,\n \n maybe_model_type = ?,\n maybe_generation_provider = ?,\n\n maybe_positive_prompt = ?,\n maybe_negative_prompt = ?,\n \n maybe_other_args = ?,\n\n creator_ip_address = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 9
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "08f52f15acf6afc7091a525e3dc88f706c71c3579e5e81aa9903807d85f469ef"
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n pci.media_token as `media_token: tokens::tokens::media_files::MediaFileToken`,\n pci.context_semantic_type as `context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension\n\nFROM prompt_context_items pci\nJOIN media_files m\nON\n pci.media_token = m.token\nWHERE\n pci.prompt_token = ?\nORDER BY pci.id ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "media_token: tokens::tokens::media_files::MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "public_bucket_directory_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "094eb099cde85835db0662c2700c6b82644b51bf9b3609391ed4d2945fe5e98f"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO prompts\nSET\n token = ?,\n prompt_type = ?,\n\n maybe_creator_user_token = ?,\n\n maybe_positive_prompt = ?,\n maybe_negative_prompt = ?,\n maybe_other_args = ?,\n\n creator_ip_address = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "13ca06bcbb4a576f8a2229a8554b7488d379baa381d8d5cb60972750e81b2173"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE generic_inference_jobs\nSET\n status = 'cancelled_by_system',\n retry_at = NULL,\n internal_debugging_failure_reason = 'reap_stale_fakeyou_jobs'\nWHERE\n job_type != 'fal_queue'\n AND status IN (\n 'pending',\n 'started',\n 'attempt_failed'\n )\n AND created_at > NOW() - INTERVAL 30 MINUTE\nLIMIT 5000\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "19708db99f2b3c6283d2115a511e0eb1c99fab396ad0effd9eb2dd79926e9c93"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO tasks (\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 8
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "22b91a21cbb88abc9bc17b338cf2f9418e08e414667d44476623eb141ab17b87"
|
||||
}
|
||||
+29
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n p.token as `token: tokens::tokens::prompts::PromptToken`,\n\n p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,\n\n p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n\n p.maybe_positive_prompt,\n p.maybe_negative_prompt,\n p.maybe_other_args,\n\n p.creator_ip_address,\n p.created_at\n\nFROM prompts as p\nWHERE\n p.token = ?\n ",
|
||||
"query": "\nSELECT\n p.token as `token: tokens::tokens::prompts::PromptToken`,\n\n p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,\n\n p.maybe_model_type as `maybe_model_type: enums::common::model_type::ModelType`,\n p.maybe_generation_provider as `maybe_generation_provider: enums::common::generation_provider::GenerationProvider`,\n\n p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n\n p.maybe_positive_prompt,\n p.maybe_negative_prompt,\n p.maybe_other_args,\n\n p.creator_ip_address,\n p.created_at\n\nFROM prompts as p\nWHERE\n p.token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -25,6 +25,26 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "maybe_model_type: enums::common::model_type::ModelType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_generation_provider: enums::common::generation_provider::GenerationProvider",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -34,7 +54,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"ordinal": 5,
|
||||
"name": "maybe_positive_prompt",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
@@ -44,7 +64,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 6,
|
||||
"name": "maybe_negative_prompt",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
@@ -54,7 +74,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 7,
|
||||
"name": "maybe_other_args",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
@@ -64,7 +84,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 8,
|
||||
"name": "creator_ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -74,7 +94,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 9,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -94,9 +114,11 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "61744bbf83f7cf00348bcc21e868d9c506fec46da4a33ac50d63c6d9e00c0cec"
|
||||
"hash": "b46b0d022790f4e10f6e47126df0fdc55172bf8cda77d4e0b2afac9e6ebd1630"
|
||||
}
|
||||
+14
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n \n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -75,6 +75,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -84,7 +94,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -105,9 +115,10 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "64dfacc1f0552d539f009fea91e902a2fe296c06ad539808596bbf2c8fc9eaa0"
|
||||
"hash": "fbe482c8987231c3b5d75f2e792d0fdd033de8a7eb821428fa034752c73edfb7"
|
||||
}
|
||||
Generated
+582
-110
File diff suppressed because it is too large
Load Diff
+7
-1
@@ -10,6 +10,7 @@ members = [
|
||||
"crates/api_clients/fal_client",
|
||||
"crates/api_clients/google_drive_common",
|
||||
"crates/api_clients/google_sign_in",
|
||||
"crates/api_clients/midjourney_client",
|
||||
"crates/api_clients/openai_sora_client",
|
||||
"crates/api_clients/openart_client",
|
||||
"crates/api_clients/runwayml_client",
|
||||
@@ -42,6 +43,7 @@ members = [
|
||||
"crates/lib/caching/redis_caching",
|
||||
"crates/lib/cloudflare_errors",
|
||||
"crates/lib/concurrency",
|
||||
"crates/lib/cookie_store",
|
||||
"crates/lib/cookies",
|
||||
"crates/lib/data/collections",
|
||||
"crates/lib/data/crockford",
|
||||
@@ -199,7 +201,8 @@ log = "0.4.27"
|
||||
once_cell = "1.21.3"
|
||||
rand = "0.9.1"
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.15", features = ["json", "cookies"] }
|
||||
reqwest = { version = "0.12.22", features = ["json", "cookies"] }
|
||||
reqwest-websocket = { version = "0.5.0" }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_derive = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
@@ -218,6 +221,7 @@ artcraft_api_defs = { path = "crates/schema/public/api/artcraft_api_defs" }
|
||||
build_metadata = { path = "crates/lib/build_metadata" }
|
||||
cloud_storage = { path = "crates/api_clients/cloud_storage" }
|
||||
cloudflare_errors = { path = "crates/lib/cloudflare_errors" }
|
||||
cookie_store = { path = "crates/lib/cookie_store" }
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
enums = { path = "crates/schema/public/enums" }
|
||||
enums_public = { path = "crates/schema/public/enums_public" }
|
||||
@@ -230,6 +234,7 @@ images = { path = "crates/lib/files/images" }
|
||||
jwt_light = { path = "crates/lib/jwt_light" }
|
||||
memory_caching = { path = "crates/lib/caching/memory_caching" }
|
||||
memory_store = { path = "crates/lib/caching/memory_store" }
|
||||
midjourney_client = { path = "crates/api_clients/midjourney_client" }
|
||||
mimetypes = { path = "crates/lib/files/mimetypes" }
|
||||
openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
|
||||
primitives = { path = "crates/lib/data/primitives" }
|
||||
@@ -238,5 +243,6 @@ sqlite_tasks = { path = "crates/schema/database/sqlite_tasks" }
|
||||
storyteller_client = { path = "crates/api_clients/storyteller_client" }
|
||||
subprocess_common = { path = "crates/lib/subprocess_common" }
|
||||
test_data = { path = "crates/lib/test_data" }
|
||||
testing = { path = "crates/lib/testing" }
|
||||
tokens = { path = "crates/schema/public/tokens" }
|
||||
web_base64 = { path = "crates/lib/data/web_base64" }
|
||||
|
||||
@@ -1,20 +1,53 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
--
|
||||
-- NB: This version needs to be bumped even if just adding or changing comments!
|
||||
-- Failure to do so will cause Windows to segfault at start. (This is horrible!)
|
||||
--
|
||||
-- NB: `app_state_dir.rs` contains the file version number.
|
||||
--
|
||||
-- Migration history:
|
||||
-- tasks_v1.sqlite - initial version
|
||||
-- tasks_v2.sqlite - added model_type (nullable)
|
||||
-- tasks_v3.sqlite - added frontend_caller, comments
|
||||
|
||||
CREATE TABLE tasks (
|
||||
-- Task auto-incrementing primary key.
|
||||
-- We don't use this.
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
|
||||
-- TaskStatus enum
|
||||
-- This is how the job system manages job states.
|
||||
-- e.g. pending, started, complete_success, dead, etc.
|
||||
task_status TEXT NOT NULL,
|
||||
|
||||
-- TaskType enum
|
||||
-- e.g. image_generation, video_generation, etc.
|
||||
task_type TEXT NOT NULL,
|
||||
|
||||
-- TaskModelType enum
|
||||
-- e.g. flux_1_dev, veo_2, etc.
|
||||
model_type TEXT,
|
||||
|
||||
-- GenerationProvider enum
|
||||
-- Together with `provider_job_id`, this creates a foreign key lookup to the job.
|
||||
-- e.g. 'artcraft', 'fal', 'sora', etc.
|
||||
provider TEXT,
|
||||
|
||||
-- The primary key for the job in the provider's system.
|
||||
provider_job_id TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- Tell the job system which caller initiated the task.
|
||||
-- e.g. 'canvas'
|
||||
frontend_caller TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- An arbitrary opaque identifier that frontend can set.
|
||||
frontend_subscriber_id TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- An opaque JSON payload set by the frontend subscriber that
|
||||
-- will be re-emitted back to the frontend.
|
||||
frontend_subscriber_payload TEXT,
|
||||
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts DROP COLUMN maybe_model_type;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts
|
||||
ADD COLUMN maybe_model_type VARCHAR(32) DEFAULT NULL;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts DROP COLUMN maybe_generation_provider;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts
|
||||
ADD COLUMN maybe_generation_provider VARCHAR(16) DEFAULT NULL;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts DROP COLUMN maybe_anonymous_visitor_token;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts
|
||||
ADD COLUMN maybe_anonymous_visitor_token VARCHAR(32) DEFAULT NULL;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE prompt_context_items;
|
||||
@@ -0,0 +1,30 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- An n:m join table between prompts and media_files.
|
||||
-- This associates mostly image media files with prompts, though we could conceivably
|
||||
-- also store 3D sets, etc.
|
||||
CREATE TABLE prompt_context_items (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- The semantic type of context.
|
||||
context_semantic_type VARCHAR(16) NOT NULL,
|
||||
|
||||
-- Foreign key, many-to-many
|
||||
-- There is no uniqueness constraint for (prompt_token, media_token) because a user
|
||||
-- might attach many duplicate images to the same prompt.
|
||||
prompt_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Foreign key to the media token
|
||||
-- There is no uniqueness constraint for (prompt_token, media_token) because a user
|
||||
-- might attach many duplicate images to the same prompt.
|
||||
media_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
KEY index_prompt_token (prompt_token),
|
||||
KEY index_media_token (media_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE generic_inference_jobs DROP COLUMN maybe_prompt_token;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE generic_inference_jobs
|
||||
ADD COLUMN maybe_prompt_token VARCHAR(32) DEFAULT NULL;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files DROP COLUMN maybe_generation_provider;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files
|
||||
ADD COLUMN maybe_generation_provider VARCHAR(16) DEFAULT NULL;
|
||||
@@ -21,6 +21,8 @@ CREATE TABLE generic_inference_jobs (
|
||||
-- The new enum for the type of job, which will eventually replace `inference_category` and `maybe_model_type`.
|
||||
-- This isn't fully supported yet in the inference-job, but we'll start populating it and adding logic around it.
|
||||
-- We'll start this out as nullable, then migrate the old rows with a default column value.
|
||||
--
|
||||
-- Fal jobs have the type `fal_queue`.
|
||||
job_type VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- There is an index on this column.
|
||||
@@ -103,6 +105,11 @@ CREATE TABLE generic_inference_jobs (
|
||||
-- And furthermore, not all download URLs have a cover image.
|
||||
maybe_cover_image_media_file_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- If we pre-constructed a prompt, this will be copied to the resultant media generated.
|
||||
-- This is especially important for Fal-related jobs and endpoints. We need this for keeping
|
||||
-- track of all the prompt details, context images, etc.
|
||||
maybe_prompt_token VARCHAR(30) DEFAULT NULL,
|
||||
|
||||
-- A migration flag we can control from the backend API service which will tell
|
||||
-- the worker where to store the results. If false, GCP. If true, AWS.
|
||||
store_in_aws BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
@@ -61,6 +61,9 @@ CREATE TABLE media_files (
|
||||
-- * 'workflow' (DEPRECATED)
|
||||
origin_product_category VARCHAR(16) NOT NULL DEFAULT "unknown",
|
||||
|
||||
-- What service provided the generation (if any).
|
||||
maybe_generation_provider VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- For inference that can be tied back to a model, the type of model.
|
||||
-- There are multiple indices on this column!
|
||||
-- DO NOT EXPOSE THIS TO USERS VIA THE API, as we may leak secrets in doing so.
|
||||
|
||||
+2
-4
@@ -15,9 +15,9 @@ CREATE TABLE batch_generations (
|
||||
|
||||
-- The type of record this is attached to
|
||||
-- Currently supported values:
|
||||
-- * 'model_weight'
|
||||
-- * 'model_weight' (deprecated)
|
||||
-- * 'comment' (deprecated)
|
||||
-- * 'media_file'
|
||||
-- * 'comment'
|
||||
entity_type VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The token for the record.
|
||||
@@ -37,6 +37,4 @@ CREATE TABLE batch_generations (
|
||||
-- NB: Realistically, we probably don't need this index until we have a lot of entity types, and the entity_token
|
||||
-- entropy is probably sufficient enough as-is.
|
||||
-- KEY index_entity_type_and_token (entity_type, entity_token),
|
||||
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,30 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- An n:m join table between prompts and media_files.
|
||||
-- This associates mostly image media files with prompts, though we could conceivably
|
||||
-- also store 3D sets, etc.
|
||||
CREATE TABLE prompt_context_items (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Foreign key, many-to-many
|
||||
-- There is no uniqueness constraint for (prompt_token, media_token) because a user
|
||||
-- might attach many duplicate images to the same prompt.
|
||||
prompt_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Foreign key to the media token
|
||||
-- There is no uniqueness constraint for (prompt_token, media_token) because a user
|
||||
-- might attach many duplicate images to the same prompt.
|
||||
media_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The semantic type of context.
|
||||
context_semantic_type VARCHAR(16) NOT NULL,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
KEY index_prompt_token (prompt_token),
|
||||
KEY index_media_token (media_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
+15
-3
@@ -19,13 +19,25 @@ CREATE TABLE prompts (
|
||||
-- Original product to which the prompt applied.
|
||||
-- Prompts may or may not be compatible across products.
|
||||
-- Possible values:
|
||||
-- * 'stable_diffusion'
|
||||
-- * 'comfy_ui'
|
||||
-- * 'artcraft_app'
|
||||
-- * 'stable_diffusion' (legacy)
|
||||
-- * 'comfy_ui' (legacy)
|
||||
prompt_type VARCHAR(16) NOT NULL,
|
||||
|
||||
-- Whomever created the prompt
|
||||
-- What type of model was used to generate the result.
|
||||
-- NB: We're conflating prompts with outputs, but in a
|
||||
-- sense the model is inextricably part of the prompt.
|
||||
maybe_model_type VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- What service provided the generation.
|
||||
maybe_generation_provider VARCHAR(16) NOT NULL,
|
||||
|
||||
-- Whoever created the prompt
|
||||
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Whoever created the prompt (anonymous user tracking)
|
||||
maybe_anonymous_visitor_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== PROMPT DETAILS ==========
|
||||
|
||||
-- The positive prompt (optional)
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::prelude::fal_ai::flux_pro::kontext::max::{max, FluxProKontextMaxInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct FluxProKontextMaxArgs<'a, U: IntoUrl, R: IntoUrl> {
|
||||
// Request
|
||||
pub prompt: &'a str,
|
||||
pub image_url: U,
|
||||
pub num_images: FluxProKontextMaxNumImages,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum FluxProKontextMaxNumImages {
|
||||
One, // Default
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
pub async fn enqueue_flux_pro_kontext_max_edit_webhook<U: IntoUrl, R: IntoUrl>(
|
||||
args: FluxProKontextMaxArgs<'_, U, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let num_images = match args.num_images {
|
||||
FluxProKontextMaxNumImages::One => 1,
|
||||
FluxProKontextMaxNumImages::Two => 2,
|
||||
FluxProKontextMaxNumImages::Three => 3,
|
||||
FluxProKontextMaxNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let request = FluxProKontextMaxInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
image_url: args.image_url.as_str().to_string(),
|
||||
num_images: Some(num_images),
|
||||
|
||||
// Maybe expose
|
||||
aspect_ratio: None,
|
||||
safety_tolerance: Some("5".to_string()), // NB: 5 is most tolerant
|
||||
output_format: Some("png".to_string()), // png or jpeg
|
||||
seed: None,
|
||||
|
||||
// Constants
|
||||
sync_mode: None, // Synchronous / slow
|
||||
};
|
||||
|
||||
let result = max(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::image::edit::enqueue_flux_pro_kontext_max_edit_webhook::{enqueue_flux_pro_kontext_max_edit_webhook, FluxProKontextMaxArgs, FluxProKontextMaxNumImages};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::TALL_MOCHI_WITH_GLASSES_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = FluxProKontextMaxArgs {
|
||||
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
|
||||
prompt: "turn the glasses into sunglasses, make them sleek sunglasses with black rims, square shaped",
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
num_images: FluxProKontextMaxNumImages::One,
|
||||
};
|
||||
|
||||
let result = enqueue_flux_pro_kontext_max_edit_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod enqueue_flux_pro_kontext_max_edit_webhook;
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::prelude::fal_ai::gpt_image_1::edit_image::byok::{gpt_text_to_image, GptTextToImageRequest};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct GptTextToImageByokArgs<'a, V: IntoUrl> {
|
||||
// Request
|
||||
pub prompt: &'a str,
|
||||
pub image_size: GptTextToImageSize,
|
||||
pub num_images: GptTextToImageNumImages,
|
||||
pub quality: GptTextToImageQuality,
|
||||
|
||||
// Fulfillment
|
||||
pub api_key: &'a FalApiKey,
|
||||
pub openai_api_key: &'a OpenAiApiKey,
|
||||
pub webhook_url: V,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptTextToImageSize{
|
||||
Auto,
|
||||
Square,
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptTextToImageQuality {
|
||||
Auto,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptTextToImageNumImages{
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
pub async fn enqueue_gpt_image_1_text_to_image_webhook<V: IntoUrl>(
|
||||
args: GptTextToImageByokArgs<'_, V>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
// auto, 1024x1024, 1536x1024, 1024x1536
|
||||
let image_size = match args.image_size {
|
||||
GptTextToImageSize::Auto => "auto",
|
||||
GptTextToImageSize::Square => "1024x1024",
|
||||
GptTextToImageSize::Horizontal => "1536x1024",
|
||||
GptTextToImageSize::Vertical => "1024x1536",
|
||||
};
|
||||
|
||||
let quality = match args.quality {
|
||||
GptTextToImageQuality::Auto => "auto",
|
||||
GptTextToImageQuality::Low => "low",
|
||||
GptTextToImageQuality::Medium => "medium",
|
||||
GptTextToImageQuality::High => "high",
|
||||
};
|
||||
|
||||
let num_images = match args.num_images {
|
||||
GptTextToImageNumImages::One => 1,
|
||||
GptTextToImageNumImages::Two => 2,
|
||||
GptTextToImageNumImages::Three => 3,
|
||||
GptTextToImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let request = GptTextToImageRequest {
|
||||
prompt: args.prompt.to_string(),
|
||||
image_size: image_size.to_string(),
|
||||
num_images,
|
||||
quality: quality.to_string(),
|
||||
openai_api_key: args.openai_api_key.0.to_string(),
|
||||
};
|
||||
|
||||
let result = gpt_text_to_image(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use crate::requests::webhook::image::enqueue_gpt_image_1_text_to_image_webhook::{enqueue_gpt_image_1_text_to_image_webhook, GptTextToImageByokArgs, GptTextToImageNumImages, GptTextToImageQuality, GptTextToImageSize};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_gpt_image_1_edit_image() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let fal_api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/openai_api_key.txt")?;
|
||||
|
||||
let open_ai_api_key = OpenAiApiKey::from_str(&secret);
|
||||
|
||||
let args = GptTextToImageByokArgs {
|
||||
prompt: "put the man and the ghost on the grassy hill. the man is scared of the friendly ghost.",
|
||||
api_key: &fal_api_key,
|
||||
openai_api_key: &open_ai_api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
image_size: GptTextToImageSize::Horizontal,
|
||||
num_images: GptTextToImageNumImages::One,
|
||||
quality: GptTextToImageQuality::High,
|
||||
};
|
||||
|
||||
let result = enqueue_gpt_image_1_text_to_image_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::prelude::fal_ai::flux_pro::v1::fill::fill;
|
||||
use fal::prelude::fal_ai::flux_pro::v1::fill::FluxProFillInput;
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct FluxPro1InfillArgs<'a, U: IntoUrl, R: IntoUrl, L: IntoUrl> {
|
||||
// Request
|
||||
pub prompt: &'a str,
|
||||
pub image_url: U,
|
||||
pub mask_url: R,
|
||||
pub num_images: FluxPro1InfillNumImages,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: L,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum FluxPro1InfillNumImages {
|
||||
One, // Default
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
pub async fn enqueue_flux_pro_1_infill_webhook<U: IntoUrl, R: IntoUrl, L: IntoUrl>(
|
||||
args: FluxPro1InfillArgs<'_, U, R, L>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let num_images = match args.num_images {
|
||||
FluxPro1InfillNumImages::One => 1,
|
||||
FluxPro1InfillNumImages::Two => 2,
|
||||
FluxPro1InfillNumImages::Three => 3,
|
||||
FluxPro1InfillNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let request = FluxProFillInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
image_url: args.image_url.as_str().to_string(),
|
||||
mask_url: args.mask_url.as_str().to_string(),
|
||||
num_images: Some(num_images),
|
||||
|
||||
// Maybe expose
|
||||
safety_tolerance: Some("5".to_string()), // NB: 5 is most tolerant
|
||||
output_format: Some("png".to_string()), // png or jpeg
|
||||
seed: None,
|
||||
|
||||
// Constants
|
||||
sync_mode: None, // Synchronous / slow
|
||||
};
|
||||
|
||||
let result = fill(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::image::infill::enqueue_flux_pro_1_infill_webhook::{enqueue_flux_pro_1_infill_webhook, FluxPro1InfillArgs, FluxPro1InfillNumImages};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::{TALL_MOCHI_WITH_GLASSES_GLASSES_MASK_IMAGE_URL, TALL_MOCHI_WITH_GLASSES_IMAGE_URL};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = FluxPro1InfillArgs {
|
||||
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
|
||||
mask_url: TALL_MOCHI_WITH_GLASSES_GLASSES_MASK_IMAGE_URL,
|
||||
prompt: "slick sunglasses, cool glasses, reflection in glasses lenses",
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
num_images: FluxPro1InfillNumImages::One,
|
||||
};
|
||||
|
||||
let result = enqueue_flux_pro_1_infill_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod enqueue_flux_pro_1_infill_webhook;
|
||||
@@ -1,6 +1,9 @@
|
||||
pub mod edit;
|
||||
pub mod enqueue_flux_1_dev_text_to_image_webhook;
|
||||
pub mod enqueue_flux_1_schnell_text_to_image_webhook;
|
||||
pub mod enqueue_flux_pro_11_text_to_image_webhook;
|
||||
pub mod enqueue_flux_pro_11_ultra_text_to_image_webhook;
|
||||
pub mod enqueue_gpt_image_1_edit_image_webhook;
|
||||
pub mod enqueue_gpt_image_1_text_to_image_webhook;
|
||||
pub mod infill;
|
||||
pub mod remove_background_rembg_webhook;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
[package]
|
||||
name = "midjourney_client"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
name = "midjourney_client"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
cloudflare_errors.workspace = true
|
||||
cookie_store.workspace = true
|
||||
errors.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
#reqwest-websocket.workspace = true
|
||||
#reqwest.workspace = true
|
||||
#reqwest-impersonate = { version = "0", features = ["websocket"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
#tokio.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
# NB: wreq gets past fingerprinting, but we have to deal with platform-specific build issues.
|
||||
#
|
||||
# Mac:
|
||||
# brew install --formula cmake pkg-config llvm
|
||||
# Windows:
|
||||
# choco install cmake -y
|
||||
# choco install strawberryperl -y
|
||||
# choco install pkgconfiglite -y
|
||||
# choco install llvm -y
|
||||
# choco install nasm -y
|
||||
wreq = { version = "5", features = ["json"] }
|
||||
wreq-util = "2"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
# NB: We need to parse HTML to get out the websocket token
|
||||
dom_query = "0.20.2"
|
||||
|
||||
[dev-dependencies]
|
||||
filesys.workspace = true
|
||||
test_data.workspace = true
|
||||
testing.workspace = true
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
/// Multiple third party Midjourney clients make this configurable.
|
||||
/// Not sure why, I'm just cargo culting it.
|
||||
#[derive(Clone)]
|
||||
pub enum MidjourneyHostname {
|
||||
Standard,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl MidjourneyHostname {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Standard => "www.midjourney.com",
|
||||
Self::Custom(domain) => domain,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod midjourney_hostname;
|
||||
pub mod midjourney_websocket_client;
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
/// One of the apparent authentication cookies used by Midjourney.
|
||||
pub (crate) const AUTH_COOKIE_NAME_R : &str = "__Host-Midjourney.AuthUserTokenV3_r";
|
||||
|
||||
/// One of the apparent authentication cookies used by Midjourney.
|
||||
pub (crate) const AUTH_COOKIE_NAME_I : &str = "__Host-Midjourney.AuthUserTokenV3_i";
|
||||
@@ -0,0 +1,8 @@
|
||||
use crate::credentials::cookie_names::{AUTH_COOKIE_NAME_I, AUTH_COOKIE_NAME_R};
|
||||
use cookie_store::cookie_store::CookieStore;
|
||||
|
||||
/// Returns true if the given cookie store has the necessary authentication cookies.
|
||||
pub fn cookie_store_has_auth_cookies(cookie_store: &CookieStore) -> bool {
|
||||
cookie_store.has_cookie(AUTH_COOKIE_NAME_I)
|
||||
&& cookie_store.has_cookie(AUTH_COOKIE_NAME_R)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
/*
|
||||
{
|
||||
"name": "u3869671173",
|
||||
"picture": "https://lh3.googleusercontent.com/a/ACg8ocKw0aaXKYSqy-Xkkn69ydIJFGnJA37S13XvjNY4d9xCzUzKUw=s96-c",
|
||||
"midjourney_id": "f8a57ac3-e416-4dd4-9be8-2c4223691b01",
|
||||
"iss": "https://securetoken.google.com/authjourney",
|
||||
"aud": "authjourney",
|
||||
"auth_time": 1754970849,
|
||||
"user_id": "bS9idl73DlSbE61KnCyKpDfazCX2",
|
||||
"sub": "bS9idl73DlSbE61KnCyKpDfazCX2",
|
||||
"iat": 1754970849,
|
||||
"exp": 1754974449,
|
||||
"email": "victorvoice89@gmail.com",
|
||||
"email_verified": true,
|
||||
"firebase": {
|
||||
"identities": {
|
||||
"google.com": [
|
||||
"116459300198619888836"
|
||||
],
|
||||
"email": [
|
||||
"victorvoice89@gmail.com"
|
||||
]
|
||||
},
|
||||
"sign_in_provider": "google.com"
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
*/
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
/// The user ID used for several midjourney requests.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct MidjourneyUserId(pub String);
|
||||
|
||||
impl MidjourneyUserId {
|
||||
pub fn from_str(user_id: &str) -> Self {
|
||||
Self(user_id.to_string())
|
||||
}
|
||||
|
||||
pub fn from_string(user_id: String) -> Self {
|
||||
Self(user_id)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod cookie_names;
|
||||
pub mod cookie_store_has_auth_cookies;
|
||||
pub mod jwt_decode;
|
||||
pub mod midjourney_user_id;
|
||||
@@ -0,0 +1,95 @@
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use crate::error::midjourney_api_error::MidjourneyApiError;
|
||||
use crate::error::midjourney_client_error::MidjourneyClientError;
|
||||
use crate::error::midjourney_error::MidjourneyError;
|
||||
use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors;
|
||||
use log::error;
|
||||
use serde::Deserialize;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
|
||||
/// This grabs the pre-rendered HTML page, which contains the websocket token and other details.
|
||||
pub struct GetIndexPageRequest {
|
||||
pub hostname: MidjourneyHostname,
|
||||
pub cookie_header: String,
|
||||
}
|
||||
|
||||
pub async fn get_index_page_html(req: GetIndexPageRequest) -> Result<String, MidjourneyError> {
|
||||
let url = format!("https://{}/", req.hostname.as_str());
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox139)
|
||||
.build()
|
||||
.map_err(|err| MidjourneyClientError::WreqError(err))?;
|
||||
|
||||
let cookie_header = req.cookie_header.trim();
|
||||
|
||||
if cookie_header.len() < 20 {
|
||||
error!("Cookie header is too short (len: {}): {}", cookie_header.len(), cookie_header);
|
||||
return Err(MidjourneyClientError::CookieTooShort.into());
|
||||
}
|
||||
|
||||
// NB: missing headers that were in the browser request:
|
||||
// -H 'sec-ch-ua-platform: "macOS"' \
|
||||
// -H 'sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"' \
|
||||
|
||||
let mut http_request = client.get(url)
|
||||
.header("cookie", cookie_header)
|
||||
.header("Referrer-Policy", "origin-when-cross-origin")
|
||||
.header("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
|
||||
.header("accept-language", "en-US,en;q=0.8")
|
||||
.header("content-type", "application/json")
|
||||
.header("priority", "u=0, i")
|
||||
.header("sec-ch-ua-mobile", "?0")
|
||||
.header("sec-fetch-dest", "document")
|
||||
.header("sec-fetch-mode", "navigate")
|
||||
.header("sec-fetch-site", "none")
|
||||
.header("sec-fetch-user", "?1")
|
||||
.header("upgrade-insecure-requests", "1");
|
||||
|
||||
let http_request = http_request
|
||||
.build()
|
||||
.map_err(|err| MidjourneyClientError::WreqError(err))?;
|
||||
|
||||
let response = client.execute(http_request)
|
||||
.await
|
||||
.map_err(|e| MidjourneyApiError::NetworkError(e.to_string()))?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let response_body = response.text().await
|
||||
.map_err(|e| MidjourneyApiError::NetworkError(e.to_string()))?;
|
||||
|
||||
if !status.is_success() {
|
||||
if let Err(err) = filter_cloudflare_errors(status.as_u16(), &response_body) {
|
||||
return Err(MidjourneyApiError::CloudflareError(err).into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response_body)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use errors::AnyhowResult;
|
||||
use filesys::read_to_trimmed_string::read_to_trimmed_string;
|
||||
use crate::endpoints::get_index_page_html::{get_index_page_html, GetIndexPageRequest};
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
let cookie_header = read_to_trimmed_string("/Users/bt/secrets/midjourney/cookie.txt")?;
|
||||
|
||||
let result = get_index_page_html(GetIndexPageRequest {
|
||||
cookie_header,
|
||||
hostname: MidjourneyHostname::Standard,
|
||||
}).await?;
|
||||
|
||||
println!("Response: {:?}\n\n", result);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use crate::credentials::midjourney_user_id::MidjourneyUserId;
|
||||
use crate::error::midjourney_api_error::MidjourneyApiError;
|
||||
use crate::error::midjourney_client_error::MidjourneyClientError;
|
||||
use crate::error::midjourney_error::MidjourneyError;
|
||||
use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors;
|
||||
use log::error;
|
||||
use serde::Deserialize;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
|
||||
const DEFAULT_PAGE_SIZE : u64 = 1000;
|
||||
|
||||
/// This returns Midjourney's job/media list
|
||||
pub struct ImagineRequest{
|
||||
pub hostname: MidjourneyHostname,
|
||||
pub cookie_header: String,
|
||||
pub user_id: MidjourneyUserId,
|
||||
pub page_size: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImagineResponse {
|
||||
pub items: Vec<ImagineItem>,
|
||||
pub cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImagineItem {
|
||||
pub id: Option<String>,
|
||||
|
||||
/// This is the prompt, e.g.
|
||||
/// `cute puppy wearing a polar bear suit --ar 4:3 --raw --v 7.0`
|
||||
pub full_command: Option<String>,
|
||||
|
||||
pub job_type: Option<JobType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum JobType {
|
||||
V6Diffusion,
|
||||
V7Diffusion,
|
||||
V7DraftRawDiffusion,
|
||||
V7RawDiffusion,
|
||||
Vid11I2vRenderAJointVideo,
|
||||
Vid11I2vRenderBJointVideo,
|
||||
Vid11I2vStartEndAVideo,
|
||||
Vid11I2vStartEndBVideo,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl JobType {
|
||||
pub fn from_str(s: &str) -> JobType {
|
||||
match s {
|
||||
"v6_diffusion" => JobType::V6Diffusion,
|
||||
"v7_diffusion" => JobType::V7Diffusion,
|
||||
"v7_draft_raw_diffusion" => JobType::V7DraftRawDiffusion,
|
||||
"v7_raw_diffusion" => JobType::V7RawDiffusion,
|
||||
"vid_1.1_i2v_render_a_joint_video" => JobType::Vid11I2vRenderAJointVideo,
|
||||
"vid_1.1_i2v_render_b_joint_video" => JobType::Vid11I2vRenderBJointVideo,
|
||||
"vid_1.1_i2v_start_end_a_video" => JobType::Vid11I2vStartEndAVideo,
|
||||
"vid_1.1_i2v_start_end_b_video" => JobType::Vid11I2vStartEndBVideo,
|
||||
other => JobType::Other(other.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn imagine(req: ImagineRequest) -> Result<ImagineResponse, MidjourneyError> {
|
||||
let cookie_header = req.cookie_header.trim();
|
||||
|
||||
if cookie_header.len() < 20 {
|
||||
error!("Cookie header is too short (len: {}): {}", cookie_header.len(), cookie_header);
|
||||
return Err(MidjourneyClientError::CookieTooShort.into());
|
||||
}
|
||||
|
||||
let page_size = req.page_size.unwrap_or(DEFAULT_PAGE_SIZE);
|
||||
|
||||
let referer = format!("https://{}/imagine", req.hostname.as_str());
|
||||
|
||||
let url = format!("https://{}/api/imagine?user_id={}&page_size={}",
|
||||
req.hostname.as_str(),
|
||||
req.user_id.as_str(),
|
||||
page_size,
|
||||
);
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox139)
|
||||
.build()
|
||||
.map_err(|err| MidjourneyClientError::WreqError(err))?;
|
||||
|
||||
// NB: missing headers that were in the browser request:
|
||||
// -H 'sec-ch-ua-platform: "macOS"' \
|
||||
// -H 'sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"' \
|
||||
|
||||
let mut http_request = client.get(url)
|
||||
.header("cookie", cookie_header)
|
||||
.header("Referer", &referer)
|
||||
.header("Referrer-Policy", "origin-when-cross-origin")
|
||||
.header("accept", "*/*")
|
||||
.header("accept-encoding", "gzip, deflate, br, zstd")
|
||||
.header("accept-language", "en-US,en;q=0.8")
|
||||
.header("content-type", "application/json")
|
||||
.header("priority", "u=1, i")
|
||||
.header("sec-ch-ua-mobile", "?0")
|
||||
.header("sec-fetch-dest", "empty")
|
||||
.header("sec-fetch-mode", "cors")
|
||||
.header("sec-fetch-site", "same-origin")
|
||||
.header("x-csrf-protection", "1");
|
||||
|
||||
let http_request = http_request
|
||||
.build()
|
||||
.map_err(|err| MidjourneyClientError::WreqError(err))?;
|
||||
|
||||
let response = client.execute(http_request)
|
||||
.await
|
||||
.map_err(|e| MidjourneyApiError::NetworkError(e.to_string()))?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let response_body = response.text().await
|
||||
.map_err(|e| MidjourneyApiError::NetworkError(e.to_string()))?;
|
||||
|
||||
if !status.is_success() {
|
||||
if let Err(err) = filter_cloudflare_errors(status.as_u16(), &response_body) {
|
||||
return Err(MidjourneyApiError::CloudflareError(err).into());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "a2339f14-ff71-43e4-a69f-8da15e7ba871",
|
||||
"enqueue_time": "2025-08-14T17:21:28.403560+00:00",
|
||||
"parent_id": null,
|
||||
"video_segments": null,
|
||||
"rating": null,
|
||||
"job_type": "v7_diffusion",
|
||||
"event_type": "diffusion",
|
||||
"parent_grid": null,
|
||||
"full_command": "a modern n64 console",
|
||||
"batch_size": 4,
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"published": true,
|
||||
"shown": true,
|
||||
"user_hidden": false,
|
||||
"template": null,
|
||||
"vid_framecount": null
|
||||
},
|
||||
],
|
||||
"cursor": "encrypted cursor",
|
||||
"checkpoint": "encrypted something ???"
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct RawImagineResponse {
|
||||
data: Vec<RawImagineItem>,
|
||||
cursor: Option<String>,
|
||||
//pub checkpoint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct RawImagineItem {
|
||||
// The job id
|
||||
id: Option<String>,
|
||||
|
||||
// The prompt plus `--` args.
|
||||
full_command: Option<String>,
|
||||
|
||||
// Number of images, typically 4
|
||||
batch_size: Option<u8>,
|
||||
|
||||
// Some possible values:
|
||||
// "v6_diffusion"
|
||||
// "v7_diffusion"
|
||||
// "v7_draft_raw_diffusion"
|
||||
// "v7_raw_diffusion"
|
||||
// "vid_1.1_i2v_render_b_joint_video",
|
||||
// "vid_1.1_i2v_start_end_a_video"
|
||||
// "vid_1.1_i2v_start_end_b_video"
|
||||
job_type: Option<String>,
|
||||
|
||||
// Some possible values:
|
||||
// "diffusion"
|
||||
// "draft"
|
||||
// "variation"
|
||||
// "video_diffusion"
|
||||
// "video_start_end"
|
||||
event_type: Option<String>,
|
||||
}
|
||||
|
||||
let response : RawImagineResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| MidjourneyApiError::DeserializationError(err))?;
|
||||
|
||||
Ok(ImagineResponse {
|
||||
cursor: response.cursor.clone(),
|
||||
items: response.data
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
ImagineItem {
|
||||
id: item.id,
|
||||
full_command: item.full_command,
|
||||
job_type: item.job_type.as_ref()
|
||||
.map(|jt| JobType::from_str(jt))
|
||||
}
|
||||
}).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use crate::credentials::midjourney_user_id::MidjourneyUserId;
|
||||
use crate::endpoints::imagine::{imagine, ImagineRequest};
|
||||
use errors::AnyhowResult;
|
||||
use filesys::read_to_trimmed_string::read_to_trimmed_string;
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
let cookie_header = read_to_trimmed_string("/Users/bt/secrets/midjourney/cookie.txt")?;
|
||||
let user_id = read_to_trimmed_string("/Users/bt/secrets/midjourney/user_id.txt")?;
|
||||
let user_id = MidjourneyUserId::from_string(user_id);
|
||||
|
||||
let result = imagine(ImagineRequest {
|
||||
cookie_header,
|
||||
hostname: MidjourneyHostname::Standard,
|
||||
user_id,
|
||||
page_size: None,
|
||||
}).await?;
|
||||
|
||||
println!("Response: {:?}\n\n", result);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod get_index_page_html;
|
||||
pub mod imagine;
|
||||
pub mod storage_list;
|
||||
pub mod submit_job;
|
||||
pub mod user_connections;
|
||||
@@ -0,0 +1,147 @@
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use crate::error::midjourney_api_error::MidjourneyApiError;
|
||||
use crate::error::midjourney_client_error::MidjourneyClientError;
|
||||
use crate::error::midjourney_error::MidjourneyError;
|
||||
use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors;
|
||||
use log::error;
|
||||
use serde::Deserialize;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
|
||||
/// This endpoint returns the user id
|
||||
pub struct GetStorageListRequest {
|
||||
pub hostname: MidjourneyHostname,
|
||||
pub cookie_header: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StorageItem {
|
||||
// NB: The path contains the user ID.
|
||||
pub bucket_pathname: Option<String>,
|
||||
|
||||
// NB: Other fields aren't necessary for us right now.
|
||||
}
|
||||
|
||||
pub async fn storage_list(req: GetStorageListRequest) -> Result<Vec<StorageItem>, MidjourneyError> {
|
||||
|
||||
let referer = format!("https://{}", req.hostname.as_str());
|
||||
|
||||
let url = format!("https://{}/api/storage-list", req.hostname.as_str());
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox139)
|
||||
.build()
|
||||
.map_err(|err| MidjourneyClientError::WreqError(err))?;
|
||||
|
||||
let cookie_header = req.cookie_header.trim();
|
||||
|
||||
if cookie_header.len() < 20 {
|
||||
error!("Cookie header is too short (len: {}): {}", cookie_header.len(), cookie_header);
|
||||
return Err(MidjourneyClientError::CookieTooShort.into());
|
||||
}
|
||||
|
||||
// NB: missing headers that were in the browser request:
|
||||
// -H 'sec-ch-ua-platform: "macOS"' \
|
||||
// -H 'sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"' \
|
||||
|
||||
let mut http_request = client.get(url)
|
||||
.header("cookie", cookie_header)
|
||||
.header("Referer", &referer)
|
||||
.header("Referrer-Policy", "origin-when-cross-origin")
|
||||
.header("accept", "*/*")
|
||||
.header("accept-language", "en-US,en;q=0.8")
|
||||
.header("content-type", "application/json")
|
||||
.header("priority", "u=1, i")
|
||||
.header("sec-ch-ua-mobile", "?0")
|
||||
.header("sec-fetch-dest", "empty")
|
||||
.header("sec-fetch-mode", "cors")
|
||||
.header("sec-fetch-site", "same-origin")
|
||||
.header("x-csrf-protection", "1");
|
||||
|
||||
let http_request = http_request
|
||||
.build()
|
||||
.map_err(|err| MidjourneyClientError::WreqError(err))?;
|
||||
|
||||
let response = client.execute(http_request)
|
||||
.await
|
||||
.map_err(|e| MidjourneyApiError::NetworkError(e.to_string()))?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
// if status == 301 {
|
||||
// for (name, value) in response.headers().iter() {
|
||||
// println!("- {}: {}", name.as_str(), value.to_str().unwrap());
|
||||
// }
|
||||
// }
|
||||
|
||||
let response_body = &response.text().await
|
||||
.map_err(|e| MidjourneyApiError::NetworkError(e.to_string()))?;
|
||||
|
||||
if !status.is_success() {
|
||||
if let Err(err) = filter_cloudflare_errors(status.as_u16(), &response_body) {
|
||||
return Err(MidjourneyApiError::CloudflareError(err).into());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
[
|
||||
{
|
||||
"bucketPathname": "UUID/OTHER_HEX_ENTROPY.png",
|
||||
"shortUrl": null,
|
||||
"type": "image-prompt",
|
||||
"origin": "web",
|
||||
"timeCreated": TIMESTAMP,
|
||||
"hidden": false,
|
||||
"contentType": "image/png",
|
||||
"state": "loaded"
|
||||
}
|
||||
]
|
||||
*/
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct RawStorageItem {
|
||||
bucketPathname: Option<String>,
|
||||
}
|
||||
|
||||
let response : Vec<RawStorageItem> = serde_json::from_str(response_body)
|
||||
.map_err(|err| MidjourneyApiError::DeserializationError(err))?;
|
||||
|
||||
let items = response
|
||||
.into_iter()
|
||||
.map(|r| StorageItem {
|
||||
bucket_pathname: r.bucketPathname,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use errors::AnyhowResult;
|
||||
use filesys::read_to_trimmed_string::read_to_trimmed_string;
|
||||
use crate::endpoints::storage_list::{storage_list, GetStorageListRequest};
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
let cookie_header = read_to_trimmed_string("/Users/bt/secrets/midjourney/cookie.txt")?;
|
||||
|
||||
let result = storage_list(GetStorageListRequest {
|
||||
cookie_header,
|
||||
hostname: MidjourneyHostname::Standard,
|
||||
}).await?;
|
||||
|
||||
println!("Response: {:?}\n\n", result);
|
||||
|
||||
for provider in result {
|
||||
println!("Provider: {:?}", provider);
|
||||
}
|
||||
|
||||
assert_eq!(1, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
use crate::error::midjourney_api_error::MidjourneyApiError;
|
||||
use crate::error::midjourney_client_error::MidjourneyClientError;
|
||||
use crate::error::midjourney_error::MidjourneyError;
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
|
||||
pub struct SubmitJobRequest<'a> {
|
||||
pub prompt: &'a str,
|
||||
pub channel_id: &'a str,
|
||||
pub hostname: MidjourneyHostname,
|
||||
pub cookie_header: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SubmitJobResponse {
|
||||
pub job_id: String,
|
||||
}
|
||||
|
||||
pub async fn submit_job(req: SubmitJobRequest<'_>) -> Result<SubmitJobResponse, MidjourneyError> {
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox139)
|
||||
.build()
|
||||
.map_err(|err| MidjourneyClientError::WreqError(err))?;
|
||||
|
||||
let referer = format!("https://{}", req.hostname.as_str());
|
||||
|
||||
// NB: Other recent clients use /api/app/submit-jobs, but this appears invalid.
|
||||
let url = format!("https://{}/api/submit-jobs", req.hostname.as_str());
|
||||
|
||||
let cookie_header = req.cookie_header.trim();
|
||||
|
||||
if cookie_header.len() < 20 {
|
||||
error!("Cookie header is too short (len: {}): {}", cookie_header.len(), cookie_header);
|
||||
return Err(MidjourneyClientError::CookieTooShort.into());
|
||||
}
|
||||
|
||||
let mut http_request = client.post(url)
|
||||
.header("cookie", cookie_header)
|
||||
.header("Referer", &referer)
|
||||
.header("Referrer-Policy", "origin-when-cross-origin")
|
||||
.header("accept", "*/*")
|
||||
.header("accept-language", "en-US,en;q=0.8")
|
||||
.header("content-type", "application/json")
|
||||
.header("priority", "u=1, i")
|
||||
.header("sec-ch-ua-mobile", "?0")
|
||||
.header("sec-fetch-dest", "empty")
|
||||
.header("sec-fetch-mode", "cors")
|
||||
.header("sec-fetch-site", "same-origin")
|
||||
.header("x-csrf-protection", "1");
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct F {
|
||||
mode: String,
|
||||
private: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct Metadata {
|
||||
imagePrompts: u8,
|
||||
imageReferences: u8,
|
||||
characterReferences: u8,
|
||||
depthReferences: u8,
|
||||
lightboxOpen: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct RawRequest {
|
||||
f: F,
|
||||
channelId: String,
|
||||
metadata: Metadata,
|
||||
t: String,
|
||||
prompt: String,
|
||||
}
|
||||
|
||||
let body = RawRequest {
|
||||
f: F {
|
||||
mode: "fast".to_string(),
|
||||
private: false,
|
||||
},
|
||||
channelId: req.channel_id.to_string(),
|
||||
metadata: Metadata {
|
||||
imagePrompts: 0,
|
||||
imageReferences: 0,
|
||||
characterReferences: 0,
|
||||
depthReferences: 0,
|
||||
lightboxOpen: "".to_string(),
|
||||
},
|
||||
t: "imagine".to_string(),
|
||||
prompt: req.prompt.to_string(),
|
||||
};
|
||||
|
||||
let http_request = http_request
|
||||
.json(&body)
|
||||
.build()
|
||||
.map_err(|err| MidjourneyClientError::WreqError(err))?;
|
||||
|
||||
let response = client.execute(http_request)
|
||||
.await
|
||||
.map_err(|e| MidjourneyApiError::NetworkError(e.to_string()))?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
// if status == 301 {
|
||||
// for (name, value) in response.headers().iter() {
|
||||
// println!("- {}: {}", name.as_str(), value.to_str().unwrap());
|
||||
// }
|
||||
// }
|
||||
|
||||
let response_body = &response.text().await
|
||||
.map_err(|e| MidjourneyApiError::NetworkError(e.to_string()))?;
|
||||
|
||||
if !status.is_success() {
|
||||
if let Err(err) = filter_cloudflare_errors(status.as_u16(), &response_body) {
|
||||
return Err(MidjourneyApiError::CloudflareError(err).into());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
{"success":[
|
||||
{
|
||||
"job_id":"UUID-like-string",
|
||||
"prompt":"prompt",
|
||||
"is_queued":false,
|
||||
"event_type":"diffusion",
|
||||
"flags":{
|
||||
"mode":"fast","visibility":"public"
|
||||
},
|
||||
"meta":{
|
||||
"height":1024,"width":1024,"batch_size":4,"parent_id":null,"parent_grid":null
|
||||
},
|
||||
"optimisticJobIndex":0,
|
||||
"personalization_codes":null
|
||||
}],
|
||||
"failure":[]}
|
||||
*/
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SuccessPayload {
|
||||
job_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct RawResponse {
|
||||
success: Vec<SuccessPayload>,
|
||||
}
|
||||
|
||||
let response = serde_json::from_str::<RawResponse>(response_body)
|
||||
.map_err(|err| MidjourneyApiError::DeserializationError(err))?;
|
||||
|
||||
let job_id = match response.success.get(0) {
|
||||
None => {
|
||||
error!("No job id found in body: {:?}", response_body);
|
||||
return Err(MidjourneyApiError::NoJobId.into());
|
||||
}
|
||||
Some(item) => item.job_id.clone(),
|
||||
};
|
||||
|
||||
Ok(SubmitJobResponse {
|
||||
job_id,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use crate::endpoints::submit_job::{submit_job, SubmitJobRequest};
|
||||
use errors::AnyhowResult;
|
||||
use filesys::read_to_trimmed_string::read_to_trimmed_string;
|
||||
|
||||
// Get channel id via:
|
||||
// https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=[TOKEN]
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
let cookie_header = read_to_trimmed_string("/Users/bt/secrets/midjourney/cookie.txt")?;
|
||||
let channel_id = read_to_trimmed_string("/Users/bt/secrets/midjourney/channel_id.txt")?;
|
||||
|
||||
let result = submit_job(SubmitJobRequest {
|
||||
prompt: "a modern n64 console",
|
||||
channel_id: &channel_id,
|
||||
cookie_header,
|
||||
hostname: MidjourneyHostname::Standard,
|
||||
}).await?;
|
||||
|
||||
println!("Response: {:?}", result);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use crate::error::midjourney_api_error::MidjourneyApiError;
|
||||
use crate::error::midjourney_client_error::MidjourneyClientError;
|
||||
use crate::error::midjourney_error::MidjourneyError;
|
||||
use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors;
|
||||
use log::error;
|
||||
use serde::Deserialize;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
|
||||
const USER_CONNECTIONS_URL :&str = "https://www.midjourney.com/api/user-connections";
|
||||
|
||||
pub struct GetUserConnectionsRequest {
|
||||
pub hostname: MidjourneyHostname,
|
||||
pub cookie_header: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserData {
|
||||
pub provider_id: String,
|
||||
pub display_name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Provider {
|
||||
pub provider_id: String,
|
||||
pub user_data: Option<UserData>,
|
||||
pub is_linked: bool,
|
||||
}
|
||||
|
||||
pub async fn user_connections(req: GetUserConnectionsRequest) -> Result<Vec<Provider>, MidjourneyError> {
|
||||
|
||||
let referer = format!("https://{}", req.hostname.as_str());
|
||||
|
||||
let url = format!("https://{}/api/user-connections", req.hostname.as_str());
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox139)
|
||||
.build()
|
||||
.map_err(|err| MidjourneyClientError::WreqError(err))?;
|
||||
|
||||
let cookie_header = req.cookie_header.trim();
|
||||
|
||||
if cookie_header.len() < 20 {
|
||||
error!("Cookie header is too short (len: {}): {}", cookie_header.len(), cookie_header);
|
||||
return Err(MidjourneyClientError::CookieTooShort.into());
|
||||
}
|
||||
|
||||
// NB: missing headers that were in the browser request:
|
||||
// -H 'sec-ch-ua-platform: "macOS"' \
|
||||
// -H 'sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"' \
|
||||
|
||||
let mut http_request = client.get(url)
|
||||
.header("cookie", cookie_header)
|
||||
.header("Referer", &referer)
|
||||
.header("Referrer-Policy", "origin-when-cross-origin")
|
||||
.header("accept", "*/*")
|
||||
.header("accept-language", "en-US,en;q=0.8")
|
||||
.header("content-type", "application/json")
|
||||
.header("priority", "u=1, i")
|
||||
.header("sec-ch-ua-mobile", "?0")
|
||||
.header("sec-fetch-dest", "empty")
|
||||
.header("sec-fetch-mode", "cors")
|
||||
.header("sec-fetch-site", "same-origin")
|
||||
.header("x-csrf-protection", "1");
|
||||
|
||||
let http_request = http_request
|
||||
.build()
|
||||
.map_err(|err| MidjourneyClientError::WreqError(err))?;
|
||||
|
||||
let response = client.execute(http_request)
|
||||
.await
|
||||
.map_err(|e| MidjourneyApiError::NetworkError(e.to_string()))?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
// if status == 301 {
|
||||
// for (name, value) in response.headers().iter() {
|
||||
// println!("- {}: {}", name.as_str(), value.to_str().unwrap());
|
||||
// }
|
||||
// }
|
||||
|
||||
let response_body = &response.text().await
|
||||
.map_err(|e| MidjourneyApiError::NetworkError(e.to_string()))?;
|
||||
|
||||
if !status.is_success() {
|
||||
if let Err(err) = filter_cloudflare_errors(status.as_u16(), &response_body) {
|
||||
return Err(MidjourneyApiError::CloudflareError(err).into());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
[
|
||||
{
|
||||
"providerId": "google.com",
|
||||
"userData": {
|
||||
"providerId": "google.com",
|
||||
"displayName": "John Doe",
|
||||
"email": "john.doe@gmail.com",
|
||||
"userId": "123456789000000001",
|
||||
},
|
||||
"isLinked": true
|
||||
},
|
||||
{
|
||||
"providerId": "discord.com",
|
||||
"isLinked": false
|
||||
}
|
||||
]
|
||||
*/
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct RawProviderUserData {
|
||||
providerId: String,
|
||||
displayName: Option<String>,
|
||||
email: Option<String>,
|
||||
userId: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct RawProvider {
|
||||
providerId: String,
|
||||
userData: Option<RawProviderUserData>,
|
||||
isLinked: bool,
|
||||
}
|
||||
|
||||
let response : Vec<RawProvider> = serde_json::from_str(response_body)
|
||||
.map_err(|err| MidjourneyApiError::DeserializationError(err))?;
|
||||
|
||||
let providers = response
|
||||
.into_iter()
|
||||
.map(|r| Provider {
|
||||
provider_id: r.providerId,
|
||||
user_data: r.userData.map(|u| UserData {
|
||||
provider_id: u.providerId,
|
||||
display_name: u.displayName,
|
||||
email: u.email,
|
||||
user_id: u.userId,
|
||||
}),
|
||||
is_linked: r.isLinked,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(providers)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use crate::endpoints::user_connections::{user_connections, GetUserConnectionsRequest};
|
||||
use errors::AnyhowResult;
|
||||
use filesys::read_to_trimmed_string::read_to_trimmed_string;
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
let cookie_header = read_to_trimmed_string("/Users/bt/secrets/midjourney/cookie.txt")?;
|
||||
|
||||
let result = user_connections(GetUserConnectionsRequest {
|
||||
cookie_header,
|
||||
hostname: MidjourneyHostname::Standard,
|
||||
}).await?;
|
||||
|
||||
println!("Response: {:?}\n\n", result);
|
||||
|
||||
for provider in result {
|
||||
println!("Provider: {:?}", provider);
|
||||
}
|
||||
|
||||
assert_eq!(1, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use errors::AnyhowError;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MidjourneyApiError {
|
||||
/// There was no job ID in the otherwise valid-looking response.
|
||||
NoJobId,
|
||||
|
||||
/// There was no user ID in an otherwise valid-looking response.
|
||||
NoUserId,
|
||||
|
||||
/// No user props in index HTML payload.
|
||||
/// We need this for the user ID and the websocket token.
|
||||
NoUserProps,
|
||||
|
||||
/// No initialAuthUser in the index HTML payload.
|
||||
/// We need this for the user ID and the websocket token.
|
||||
NoInitialAuthUser,
|
||||
|
||||
/// 400. The request was invalid.
|
||||
InvalidRequest(String),
|
||||
|
||||
/// 401. The request was not authorized.
|
||||
Unauthorized(String),
|
||||
|
||||
/// 403. The request was forbidden.
|
||||
Forbidden(String),
|
||||
|
||||
/// 404. The requested resource was not found.
|
||||
NotFound(String),
|
||||
|
||||
/// 429. Too many requests.
|
||||
TooManyRequests(String),
|
||||
|
||||
/// 500. An internal server error occurred.
|
||||
InternalServerError {
|
||||
body: String,
|
||||
backend_hostname: Option<String>,
|
||||
},
|
||||
|
||||
/// Eg. when downloading images
|
||||
UnknownHttpFailure {
|
||||
status_code: u16,
|
||||
body: String,
|
||||
},
|
||||
|
||||
/// Cloudflare errors.
|
||||
CloudflareError(CloudflareError),
|
||||
|
||||
/// A deserialization error with the response.
|
||||
DeserializationError(serde_json::Error),
|
||||
|
||||
/// The request timed out.
|
||||
Timeout(String),
|
||||
|
||||
/// A network error occurred.
|
||||
NetworkError(String),
|
||||
|
||||
// /// Uncategorized reqwest error.
|
||||
// OtherReqwestError(reqwest::Error),
|
||||
|
||||
/// An error doing file I/O (on our side)
|
||||
IoError(io::Error),
|
||||
|
||||
/// Another type of error.
|
||||
Other(AnyhowError),
|
||||
}
|
||||
|
||||
impl Error for MidjourneyApiError {}
|
||||
|
||||
impl Display for MidjourneyApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
// Response body errors
|
||||
Self::NoJobId => write!(f, "No job ID found in the response body."),
|
||||
Self::NoUserId => write!(f, "No user ID found in the response body."),
|
||||
Self::NoUserProps => write!(f, "No user properties found in the index HTML payload."),
|
||||
Self::NoInitialAuthUser => write!(f, "No initialAuthUser in the index HTML payload."),
|
||||
// Server response code errors
|
||||
Self::InvalidRequest(msg) => write!(f, "Invalid request: {}", msg),
|
||||
Self::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
|
||||
Self::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
|
||||
Self::NotFound(msg) => write!(f, "Not found: {}", msg),
|
||||
Self::TooManyRequests(msg) => write!(f, "Too many requests: {}", msg),
|
||||
Self::InternalServerError {body, backend_hostname} =>
|
||||
write!(f, "Internal Server Error; backend hostname: {:?} ; body: {}; ", backend_hostname, body),
|
||||
Self::UnknownHttpFailure {status_code, body} =>
|
||||
write!(f, "Unknown HTTP failure; status code: {}; body: {}", status_code, body),
|
||||
// Deserialization errors
|
||||
// Server response handling errors
|
||||
Self::DeserializationError(error) => write!(f, "Deserialization error: {}", error),
|
||||
// Network errors
|
||||
Self::Timeout(msg) => write!(f, "Timeout: {}", msg),
|
||||
Self::NetworkError(msg) => write!(f, "Network error: {}", msg),
|
||||
// Cloudflare errors
|
||||
Self::CloudflareError(error) => write!(f, "Cloudflare Error: {}", error),
|
||||
// I/O errors
|
||||
Self::IoError(error) => write!(f, "IO error: {}", error),
|
||||
// Other
|
||||
// Self::OtherReqwestError(error) => write!(f, "Reqwest error: {}", error),
|
||||
Self::Other(error) => write!(f, "Other error: {}", error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*impl From<reqwest::Error> for MidjourneyApiError {
|
||||
fn from(error: reqwest::Error) -> Self {
|
||||
if error.is_timeout() {
|
||||
Self::Timeout(error.to_string())
|
||||
} else if error.is_connect() {
|
||||
Self::NetworkError(error.to_string())
|
||||
} else {
|
||||
Self::OtherReqwestError(error)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
impl From<serde_json::Error> for MidjourneyApiError {
|
||||
fn from(error: serde_json::Error) -> Self {
|
||||
Self::DeserializationError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for MidjourneyApiError {
|
||||
fn from(error: io::Error) -> Self {
|
||||
Self::IoError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CloudflareError> for MidjourneyApiError {
|
||||
fn from(error: CloudflareError) -> Self {
|
||||
Self::CloudflareError(error)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// An error arising from the client, either before or after the request is sent.
|
||||
#[derive(Debug)]
|
||||
pub enum MidjourneyClientError {
|
||||
/// The cookie was too short to be valid.
|
||||
CookieTooShort,
|
||||
|
||||
/// Tried to look up an image out of range [0,4)
|
||||
InvalidImageIndex,
|
||||
|
||||
/// Could not determine the filetype for the file.
|
||||
FileTypeNotKnown(PathBuf),
|
||||
|
||||
/// Could not handle the file type.
|
||||
FileTypeNotHandled(PathBuf),
|
||||
|
||||
/// An error doing file I/O (on our side)
|
||||
IoError(io::Error),
|
||||
|
||||
// /// An error setting up the client
|
||||
// ReqwestError(reqwest::Error),
|
||||
WreqError(wreq::Error),
|
||||
|
||||
/// A serialization error with the request.
|
||||
SerializationError(serde_json::Error),
|
||||
}
|
||||
|
||||
impl Error for MidjourneyClientError {}
|
||||
|
||||
impl Display for MidjourneyClientError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CookieTooShort => write!(f, "The cookie header is too short to be valid."),
|
||||
Self::InvalidImageIndex => write!(f, "The image index is out of range. Valid range is [0, 4)."),
|
||||
Self::FileTypeNotKnown(path) => write!(f, "Could not determine the filetype for the file: {:?}", path),
|
||||
Self::FileTypeNotHandled(path) => write!(f, "Could not handle the file type for the file: {:?}", path),
|
||||
Self::IoError(err) => write!(f, "IO error: {}", err),
|
||||
//Self::ReqwestError(err) => write!(f, "Reqwest client error: {}", err),
|
||||
Self::WreqError(err) => write!(f, "Wreq client error: {}", err),
|
||||
Self::SerializationError(err) => write!(f, "Request serialization error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for MidjourneyClientError {
|
||||
fn from(error: io::Error) -> Self {
|
||||
Self::IoError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for MidjourneyClientError {
|
||||
fn from(error: serde_json::Error) -> Self {
|
||||
Self::SerializationError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/*impl From<reqwest::Error> for MidjourneyClientError {
|
||||
fn from(error: reqwest::Error) -> Self {
|
||||
Self::ReqwestError(error)
|
||||
}
|
||||
}*/
|
||||
|
||||
impl From<wreq::Error> for MidjourneyClientError {
|
||||
fn from(error: wreq::Error) -> Self {
|
||||
Self::WreqError(error)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use crate::error::midjourney_api_error::MidjourneyApiError;
|
||||
use crate::error::midjourney_client_error::MidjourneyClientError;
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MidjourneyError {
|
||||
Api(MidjourneyApiError),
|
||||
Client(MidjourneyClientError),
|
||||
}
|
||||
|
||||
impl Error for MidjourneyError {}
|
||||
|
||||
impl std::fmt::Display for MidjourneyError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Api(e) => write!(f, "Midjourney API error: {:?}", e),
|
||||
Self::Client(e) => write!(f, "Midjourney Client error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MidjourneyApiError> for MidjourneyError {
|
||||
fn from(error: MidjourneyApiError) -> Self {
|
||||
Self::Api(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MidjourneyClientError> for MidjourneyError {
|
||||
fn from(error: MidjourneyClientError) -> Self {
|
||||
Self::Client(error)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod midjourney_error;
|
||||
pub mod midjourney_api_error;
|
||||
pub mod midjourney_client_error;
|
||||
@@ -0,0 +1,8 @@
|
||||
//! A midjourney API client.
|
||||
|
||||
pub mod client;
|
||||
pub mod credentials;
|
||||
pub mod endpoints;
|
||||
pub mod error;
|
||||
pub mod recipes;
|
||||
pub mod utils;
|
||||
@@ -0,0 +1,20 @@
|
||||
use crate::credentials::midjourney_user_id::MidjourneyUserId;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ChannelId {
|
||||
/// Channel based on the user ID.
|
||||
/// This is what Midjourney uses by default.
|
||||
UserId(MidjourneyUserId),
|
||||
|
||||
// Bare string ID
|
||||
Raw(String),
|
||||
}
|
||||
|
||||
impl ChannelId {
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::UserId(user_id) => format!("singleplayer_{}", user_id.0),
|
||||
Self::Raw(id) => id.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use crate::credentials::midjourney_user_id::MidjourneyUserId;
|
||||
use crate::endpoints::storage_list::{storage_list, GetStorageListRequest};
|
||||
use crate::error::midjourney_api_error::MidjourneyApiError;
|
||||
use crate::error::midjourney_error::MidjourneyError;
|
||||
|
||||
pub struct GetMidjourneyUserIdRequest {
|
||||
pub hostname: MidjourneyHostname,
|
||||
pub cookie_header: String,
|
||||
}
|
||||
|
||||
pub async fn get_midjourney_user_id(req: GetMidjourneyUserIdRequest) -> Result<MidjourneyUserId, MidjourneyError> {
|
||||
let items = storage_list(GetStorageListRequest {
|
||||
hostname: req.hostname,
|
||||
cookie_header: req.cookie_header,
|
||||
}).await?;
|
||||
|
||||
let user_id = items
|
||||
.into_iter()
|
||||
.find_map(|item| item.bucket_pathname)
|
||||
.and_then(|path| path.split('/').next().map(|id| id.to_owned()))
|
||||
.map(|id| MidjourneyUserId(id.to_string()))
|
||||
.ok_or(MidjourneyApiError::NoUserId)?;
|
||||
|
||||
Ok(user_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use crate::recipes::get_midjourney_user_id::{get_midjourney_user_id, GetMidjourneyUserIdRequest};
|
||||
use errors::AnyhowResult;
|
||||
use filesys::read_to_trimmed_string::read_to_trimmed_string;
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
let cookie_header = read_to_trimmed_string("/Users/bt/secrets/midjourney/cookie.txt")?;
|
||||
|
||||
let result = get_midjourney_user_id(GetMidjourneyUserIdRequest {
|
||||
cookie_header,
|
||||
hostname: MidjourneyHostname::Standard,
|
||||
}).await?;
|
||||
|
||||
println!("Response: {:?}\n\n", result);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use crate::credentials::midjourney_user_id::MidjourneyUserId;
|
||||
use crate::endpoints::get_index_page_html::{get_index_page_html, GetIndexPageRequest};
|
||||
use crate::error::midjourney_api_error::MidjourneyApiError;
|
||||
use crate::error::midjourney_error::MidjourneyError;
|
||||
use dom_query::Document;
|
||||
use serde::Deserialize;
|
||||
|
||||
pub struct GetUserInfoRequest {
|
||||
pub hostname: MidjourneyHostname,
|
||||
pub cookie_header: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GetUserInfoResponse {
|
||||
pub user_id: Option<MidjourneyUserId>,
|
||||
pub email: Option<String>,
|
||||
pub websocket_token: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_user_info(req: GetUserInfoRequest) -> Result<GetUserInfoResponse, MidjourneyError> {
|
||||
let index_html = get_index_page_html(GetIndexPageRequest {
|
||||
hostname: req.hostname,
|
||||
cookie_header: req.cookie_header,
|
||||
}).await?;
|
||||
|
||||
/*
|
||||
<script id="initialProps" type="application/json">
|
||||
{
|
||||
"initialAuthUser": {
|
||||
"idpID": "ENTROPY",
|
||||
"id": "UUID",
|
||||
"displayName": "uNUMBER",
|
||||
"email": "USER@gmail.com",
|
||||
"emailVerified": true,
|
||||
"photoURL": "https://lh3.googleusercontent.com/a/ENTROPY",
|
||||
"abilities": {
|
||||
"admin": false,
|
||||
"developer": false,
|
||||
"accepted_tos": true,
|
||||
"moderator": false,
|
||||
"guide": false,
|
||||
"vip": false,
|
||||
"employee": false,
|
||||
"content_moderator": false,
|
||||
"gdpr_preferences_set": false,
|
||||
"gdpr_allow_advertising": false,
|
||||
"gdpr_allow_analytics": false,
|
||||
"editor_whitelist": false,
|
||||
"allow_nsfw": false,
|
||||
"cooldowns_removed": false,
|
||||
"blocked": false,
|
||||
"community": false,
|
||||
"billing": false,
|
||||
"system_verified": false,
|
||||
"simplified_flow": false,
|
||||
"total_jobs": 57,
|
||||
"is_sub_at_least_one_year_old": false,
|
||||
"can_private": false,
|
||||
"can_relax": false,
|
||||
"has_discord": false,
|
||||
"delete_at": null,
|
||||
"subscription": {
|
||||
"type": "subscription",
|
||||
"plan_type": "basic",
|
||||
"recurring": "month"
|
||||
},
|
||||
"relax_video_job_concurrency": 0
|
||||
},
|
||||
"websocketToken": "WEBSOCKET_TOKEN_ENTROPY",
|
||||
"providers": [
|
||||
"google.com"
|
||||
],
|
||||
"system": "firebase",
|
||||
"streamToken": "STREAM_TOKEN_ENTROPY"
|
||||
},
|
||||
"urlFromInitialServerRequest": "/"
|
||||
}
|
||||
</script>
|
||||
*/
|
||||
|
||||
let document = Document::from(index_html);
|
||||
let maybe_nodes = document.select("script[id=initialProps]");
|
||||
|
||||
let node = match maybe_nodes.get(0) {
|
||||
Some(node) => node,
|
||||
None => {
|
||||
return Err(MidjourneyApiError::NoUserProps.into());
|
||||
}
|
||||
};
|
||||
|
||||
let inner_json = node.inner_html().to_string();
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct RawAuthUser {
|
||||
// NB: The Midjourney user id.
|
||||
id: Option<String>,
|
||||
email: Option<String>,
|
||||
websocketToken: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct RawBody {
|
||||
initialAuthUser: Option<RawAuthUser>,
|
||||
}
|
||||
|
||||
let response : RawBody = serde_json::from_str(&inner_json)
|
||||
.map_err(|err| MidjourneyApiError::DeserializationError(err))?;
|
||||
|
||||
let props = match response.initialAuthUser {
|
||||
Some(props) => props,
|
||||
None => {
|
||||
return Err(MidjourneyApiError::NoInitialAuthUser.into());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(GetUserInfoResponse {
|
||||
user_id: props.id
|
||||
.map(|id| MidjourneyUserId::from_string(id)),
|
||||
email: props.email
|
||||
.map(|email| email.to_string()),
|
||||
websocket_token: props.websocketToken
|
||||
.map(|email| email.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::client::midjourney_hostname::MidjourneyHostname;
|
||||
use crate::recipes::get_user_info::{get_user_info, GetUserInfoRequest};
|
||||
use errors::AnyhowResult;
|
||||
use filesys::read_to_trimmed_string::read_to_trimmed_string;
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
let cookie_header = read_to_trimmed_string("/Users/bt/secrets/midjourney/cookie.txt")?;
|
||||
|
||||
let result = get_user_info(GetUserInfoRequest {
|
||||
cookie_header,
|
||||
hostname: MidjourneyHostname::Standard,
|
||||
}).await?;
|
||||
|
||||
println!("Response: {:?}\n\n", result);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mod get_midjourney_user_id;
|
||||
pub mod channel_id;
|
||||
pub mod get_user_info;
|
||||
@@ -0,0 +1,9 @@
|
||||
use crate::error::midjourney_client_error::MidjourneyClientError;
|
||||
use crate::error::midjourney_error::MidjourneyError;
|
||||
|
||||
pub fn get_image_url(job_id: &str, image_index: u8) -> Result<String, MidjourneyError> {
|
||||
if image_index > 3 {
|
||||
return Err(MidjourneyClientError::InvalidImageIndex.into())
|
||||
}
|
||||
Ok(format!("https://cdn.midjourney.com/{}/0_{}.png", job_id, image_index))
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use crate::error::midjourney_api_error::MidjourneyApiError;
|
||||
use crate::error::midjourney_client_error::MidjourneyClientError;
|
||||
use crate::error::midjourney_error::MidjourneyError;
|
||||
use crate::utils::get_image_url::get_image_url;
|
||||
use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ImageDownloaderClient {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl ImageDownloaderClient {
|
||||
pub fn create() -> Result<Self, MidjourneyClientError> {
|
||||
Ok(Self {
|
||||
client: Client::builder()
|
||||
.emulation(Emulation::Firefox139)
|
||||
.build()
|
||||
.map_err(|err| MidjourneyClientError::WreqError(err))?
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn download_image(&self, job_id: &str, image_index: u8) -> anyhow::Result<Vec<u8>, MidjourneyError> {
|
||||
let url = get_image_url(job_id, image_index)?;
|
||||
|
||||
// TODO: Cookies
|
||||
// TODO: Cache control headers?
|
||||
let mut http_request = self.client.get(url)
|
||||
//.header("Referrer-Policy", "origin-when-cross-origin")
|
||||
//.header("content-type", "application/json")
|
||||
//.header("sec-fetch-user", "?1")
|
||||
//.header("upgrade-insecure-requests", "1");
|
||||
.header("Referrer", "https://www.midjourney.com/")
|
||||
.header("accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
|
||||
.header("accept-language", "en-US,en;q=0.8")
|
||||
.header("priority", "i")
|
||||
.header("sec-ch-ua-mobile", "?0")
|
||||
.header("sec-fetch-dest", "image")
|
||||
.header("sec-fetch-mode", "no-cors")
|
||||
.header("sec-fetch-site", "same-site");
|
||||
|
||||
let http_request = http_request
|
||||
.build()
|
||||
.map_err(|err| MidjourneyClientError::WreqError(err))?;
|
||||
|
||||
let response = self.client.execute(http_request)
|
||||
.await
|
||||
.map_err(|e| MidjourneyApiError::NetworkError(e.to_string()))?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let response_bytes = response.bytes().await
|
||||
.map_err(|e| MidjourneyApiError::NetworkError(e.to_string()))?;
|
||||
|
||||
if !status.is_success() {
|
||||
let response_body = String::from_utf8_lossy(&response_bytes).to_string();
|
||||
if let Err(err) = filter_cloudflare_errors(status.as_u16(), &response_body) {
|
||||
return Err(MidjourneyApiError::CloudflareError(err).into());
|
||||
}
|
||||
|
||||
return Err(MidjourneyApiError::UnknownHttpFailure {
|
||||
status_code: status.as_u16(),
|
||||
body: response_body,
|
||||
}.into());
|
||||
}
|
||||
|
||||
Ok(response_bytes.to_vec())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod get_image_url;
|
||||
pub mod image_downloader_client;
|
||||
@@ -59,7 +59,7 @@ impl Display for ApiError {
|
||||
ApiError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
|
||||
ApiError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
|
||||
ApiError::NotFound(msg) => write!(f, "Not found: {}", msg),
|
||||
ApiError::TooManyRequests(msg) => write!(f, "Too many requests: {}", msg),
|
||||
ApiError::TooManyRequests(msg) => write!(f, "Too many requests to Storyteller backend: {}", msg),
|
||||
ApiError::InternalServerError {body, backend_hostname} =>
|
||||
write!(f, "Internal Server Error; backend hostname: {:?} ; body: {}; ", backend_hostname, body),
|
||||
// Server response handling errors
|
||||
|
||||
@@ -13,8 +13,8 @@ impl Error for StorytellerError {}
|
||||
impl std::fmt::Display for StorytellerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
StorytellerError::Api(e) => write!(f, "API error: {:?}", e),
|
||||
StorytellerError::Client(e) => write!(f, "Client error: {:?}", e),
|
||||
StorytellerError::Api(e) => write!(f, "Storyteller API error: {:?}", e),
|
||||
StorytellerError::Client(e) => write!(f, "Storyteller Client error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{FluxProKontextMaxEditImageRequest, FluxProKontextMaxEditImageResponse, FLUX_PRO_KONTEXT_MAX_EDIT_IMAGE_PATH};
|
||||
|
||||
|
||||
pub async fn flux_pro_kontext_max_edit_image(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: FluxProKontextMaxEditImageRequest,
|
||||
) -> Result<FluxProKontextMaxEditImageResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
FLUX_PRO_KONTEXT_MAX_EDIT_IMAGE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod flux_pro_kontext_max_edit_image;
|
||||
pub mod gpt_image_1_edit_image;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageRequest, GenerateGptImage1TextToImageResponse, GPT_IMAGE_1_TEXT_TO_IMAGE_PATH};
|
||||
|
||||
|
||||
pub async fn generate_gpt_image_1_text_to_image(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateGptImage1TextToImageRequest,
|
||||
) -> Result<GenerateGptImage1TextToImageResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GPT_IMAGE_1_TEXT_TO_IMAGE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::image::inpaint::flux_pro_1_inpaint_image::{FluxPro1InpaintImageRequest, FluxPro1InpaintImageResponse, FLUX_PRO_1_INPAINT_PATH};
|
||||
|
||||
pub async fn flux_pro_1_inpaint_image(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: FluxPro1InpaintImageRequest,
|
||||
) -> Result<FluxPro1InpaintImageResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
FLUX_PRO_1_INPAINT_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod flux_pro_1_inpaint_image;
|
||||
@@ -3,4 +3,6 @@ pub mod generate_flux_1_dev_text_to_image;
|
||||
pub mod generate_flux_1_schnell_text_to_image;
|
||||
pub mod generate_flux_pro_11_text_to_image;
|
||||
pub mod generate_flux_pro_11_ultra_text_to_image;
|
||||
pub mod generate_gpt_image_1_text_to_image;
|
||||
pub mod inpaint;
|
||||
pub mod remove_image_background;
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod get_media_file;
|
||||
pub mod upload_image_media_file_from_bytes;
|
||||
pub mod upload_image_media_file_from_file;
|
||||
pub mod upload_new_engine_asset_from_file;
|
||||
pub mod upload_video_media_file_from_file;
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails;
|
||||
use crate::shared_response_types::media_links::MediaLinks;
|
||||
use crate::shared_response_types::simple_entity_stats::SimpleEntityStats;
|
||||
use crate::shared_response_types::user_details_light::UserDetailsLight;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::http_get_anonymous::http_get_anonymous;
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::by_table::model_weights::weights_category::WeightsCategory;
|
||||
use enums::common::visibility::Visibility;
|
||||
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use reqwest::Client;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::model_weights::ModelWeightToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use uuid::uuid;
|
||||
|
||||
pub struct UploadImageBytesArgs<'a> {
|
||||
pub api_host: &'a ApiHost,
|
||||
pub maybe_creds: Option<&'a StorytellerCredentialSet>,
|
||||
|
||||
// NB: Bytes need to be owned for the request.
|
||||
pub image_bytes: Vec<u8>,
|
||||
pub image_type: ImageType,
|
||||
|
||||
/// If true, we should hide the image from the user's gallery.
|
||||
pub is_intermediate_system_file: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ImageType {
|
||||
Png,
|
||||
Jpeg,
|
||||
Gif,
|
||||
}
|
||||
|
||||
impl ImageType {
|
||||
pub fn mime_type(&self) -> &str {
|
||||
match self {
|
||||
ImageType::Png => "image/png",
|
||||
ImageType::Jpeg => "image/jpeg",
|
||||
ImageType::Gif => "image/gif",
|
||||
}
|
||||
}
|
||||
pub fn file_extension(&self) -> &str {
|
||||
match self {
|
||||
ImageType::Png => "png",
|
||||
ImageType::Jpeg => "jpg",
|
||||
ImageType::Gif => "gif",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload an image media file from a file.
|
||||
/// NB: File type is not checked, so caller needs to enforce.
|
||||
/// NB: We need owned bytes for the request.
|
||||
pub async fn upload_image_media_file_from_bytes(
|
||||
args: UploadImageBytesArgs<'_>
|
||||
) -> Result<UploadImageMediaFileSuccessResponse, StorytellerError> {
|
||||
|
||||
let url = get_route(args.api_host);
|
||||
|
||||
debug!("Requesting {:?}", &url);
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let part = Part::bytes(args.image_bytes)
|
||||
.file_name(format!("image.{}", args.image_type.file_extension()))
|
||||
.mime_str(args.image_type.mime_type())
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let mut form = Form::new()
|
||||
.text("uuid_idempotency_token", generate_random_uuid())
|
||||
.part("file", part);
|
||||
|
||||
if args.is_intermediate_system_file {
|
||||
form = form.text("is_intermediate_system_file", "true");
|
||||
}
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON);
|
||||
|
||||
if let Some(creds) = args.maybe_creds {
|
||||
if let Some(header) = &creds.maybe_as_cookie_header() {
|
||||
request_builder = request_builder.header("Cookie", header);
|
||||
}
|
||||
}
|
||||
|
||||
let response = request_builder
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response = filter_bad_response(response).await?;
|
||||
let response_body = &response.text().await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let media_file = serde_json::from_str(&response_body)
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
Ok(media_file)
|
||||
}
|
||||
|
||||
fn get_route(api_host: &ApiHost) -> String {
|
||||
let api_hostname_and_scheme = api_host.to_api_hostname_and_scheme();
|
||||
format!("{}/v1/media_files/upload/image", api_hostname_and_scheme)
|
||||
}
|
||||
|
||||
// TODO(bt,2025-04-22): Share API definitions between client and server in common crate.
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct UploadImageMediaFileSuccessResponse {
|
||||
pub success: bool,
|
||||
pub media_file_token: MediaFileToken,
|
||||
}
|
||||
+21
-7
@@ -33,14 +33,24 @@ use tokens::tokens::model_weights::ModelWeightToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use uuid::uuid;
|
||||
|
||||
pub struct UploadImageFromFileArgs<'a, P: AsRef<Path>> {
|
||||
pub api_host: &'a ApiHost,
|
||||
pub maybe_creds: Option<&'a StorytellerCredentialSet>,
|
||||
|
||||
// NB: Path needs to be owned for the request.
|
||||
pub path: P,
|
||||
|
||||
/// If true, we should hide the image from the user's gallery.
|
||||
pub is_intermediate_system_file: bool,
|
||||
}
|
||||
|
||||
|
||||
/// Upload an image media file from a file.
|
||||
pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
path: P,
|
||||
args: UploadImageFromFileArgs<'_, P>,
|
||||
) -> Result<UploadImageMediaFileSuccessResponse, StorytellerError> {
|
||||
|
||||
let url = get_route(api_host);
|
||||
let url = get_route(args.api_host);
|
||||
|
||||
debug!("Requesting {:?}", &url);
|
||||
|
||||
@@ -51,17 +61,21 @@ pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let form = Form::new()
|
||||
let mut form = Form::new()
|
||||
.text("uuid_idempotency_token", generate_random_uuid())
|
||||
.file("file", path)
|
||||
.file("file", args.path)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
if args.is_intermediate_system_file {
|
||||
form = form.text("is_intermediate_system_file", "true");
|
||||
}
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON);
|
||||
|
||||
if let Some(creds) = maybe_creds {
|
||||
if let Some(creds) = args.maybe_creds {
|
||||
if let Some(header) = &creds.maybe_as_cookie_header() {
|
||||
request_builder = request_builder.header("Cookie", header);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
|
||||
use crate::media_files::upload_image_media_file_from_file::{upload_image_media_file_from_file, UploadImageFromFileArgs};
|
||||
use crate::media_files::upload_new_engine_asset_from_file::upload_new_engine_asset_from_file;
|
||||
use crate::media_files::upload_video_media_file_from_file::upload_video_media_file_from_file;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
@@ -49,7 +49,13 @@ pub async fn upload_media_file_from_file<P: AsRef<Path>>(
|
||||
}
|
||||
}
|
||||
Some(FileExtension::Png) | Some(FileExtension::Jpg) | Some(FileExtension::Gif) | Some(FileExtension::Webp) => {
|
||||
match upload_image_media_file_from_file(api_host, maybe_creds, path).await {
|
||||
let result = upload_image_media_file_from_file(UploadImageFromFileArgs {
|
||||
api_host,
|
||||
maybe_creds,
|
||||
path,
|
||||
is_intermediate_system_file: false,
|
||||
}).await;
|
||||
match result {
|
||||
Ok(result) => Ok(UploadMediaFileSuccessResponse {
|
||||
success: result.success,
|
||||
media_file_token: result.media_file_token,
|
||||
|
||||
@@ -23,24 +23,27 @@ tauri-build = { version = "2.1.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
artcraft_api_defs = { workspace = true }
|
||||
build_metadata = { workspace = true }
|
||||
enums = { workspace = true }
|
||||
errors = { workspace = true }
|
||||
fal_client = { workspace = true }
|
||||
filesys = { workspace = true }
|
||||
idempotency = { workspace = true }
|
||||
memory_store = { workspace = true }
|
||||
mimetypes = { workspace = true }
|
||||
openai_sora_client = { workspace = true }
|
||||
primitives = { workspace = true }
|
||||
artcraft_api_defs.workspace = true
|
||||
build_metadata.workspace = true
|
||||
cookie_store.workspace = true
|
||||
enums.workspace = true
|
||||
errors.workspace = true
|
||||
fal_client.workspace = true
|
||||
filesys.workspace = true
|
||||
idempotency.workspace = true
|
||||
images.workspace = true
|
||||
memory_store.workspace = true
|
||||
midjourney_client.workspace = true
|
||||
mimetypes.workspace = true
|
||||
openai_sora_client.workspace = true
|
||||
primitives.workspace = true
|
||||
sqlite_tasks.workspace = true
|
||||
storyteller_client = { workspace = true }
|
||||
tokens = { workspace = true }
|
||||
web_base64 = { workspace = true }
|
||||
storyteller_client.workspace = true
|
||||
tokens.workspace = true
|
||||
web_base64.workspace = true
|
||||
|
||||
# External
|
||||
anyhow = { workspace = true }
|
||||
|
||||
+29
-2
@@ -1,7 +1,9 @@
|
||||
use crate::core::commands::enqueue::image::generic::handle_image_artcraft::handle_image_artcraft;
|
||||
use crate::core::commands::enqueue::image::generic::handle_image_fal::handle_image_fal;
|
||||
use crate::core::commands::enqueue::image::gpt_image_1::handle_image_sora::handle_image_sora;
|
||||
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1::handle_gpt_image_1;
|
||||
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image::midjourney::handle_midjourney::handle_midjourney;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::Response;
|
||||
@@ -17,6 +19,7 @@ use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -39,6 +42,9 @@ pub struct EnqueueTextToImageRequest {
|
||||
|
||||
/// Aspect ratio.
|
||||
pub aspect_ratio: Option<TextToImageSize>,
|
||||
|
||||
/// The number of images to generate.
|
||||
pub number_images: Option<u32>,
|
||||
}
|
||||
|
||||
// TODO(bt,2025-07-14): Support other aspect ratios / resolutions -
|
||||
@@ -89,6 +95,7 @@ pub async fn enqueue_text_to_image_command(
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
task_database: State<'_, TaskDatabase>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
mj_creds_manager: State<'_, MidjourneyCredentialManager>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
@@ -104,6 +111,7 @@ pub async fn enqueue_text_to_image_command(
|
||||
&provider_priority_store,
|
||||
&task_database,
|
||||
&fal_creds_manager,
|
||||
&mj_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&app_env_configs,
|
||||
&fal_task_queue,
|
||||
@@ -169,6 +177,7 @@ pub async fn handle_request(
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
task_database: &TaskDatabase,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
mj_creds_manager: &MidjourneyCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
@@ -185,6 +194,7 @@ pub async fn handle_request(
|
||||
&storyteller_creds_manager,
|
||||
&app_env_configs,
|
||||
&fal_task_queue,
|
||||
&mj_creds_manager,
|
||||
&sora_creds_manager,
|
||||
&sora_task_queue,
|
||||
).await;
|
||||
@@ -215,6 +225,7 @@ pub async fn dispatch_request(
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
mj_creds_manager: &MidjourneyCredentialManager,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, InternalImageError> {
|
||||
@@ -224,7 +235,23 @@ pub async fn dispatch_request(
|
||||
return Err(InternalImageError::NoModelSpecified);
|
||||
}
|
||||
Some(ImageModel::GptImage1) => {
|
||||
return handle_image_sora(&app, request, sora_creds_manager, sora_task_queue).await;
|
||||
return handle_gpt_image_1(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await;
|
||||
}
|
||||
Some(ImageModel::Midjourney) => {
|
||||
return handle_midjourney(
|
||||
request,
|
||||
app_env_configs,
|
||||
mj_creds_manager,
|
||||
).await;
|
||||
}
|
||||
_ => {
|
||||
// Fall-through
|
||||
|
||||
+73
-46
@@ -9,10 +9,10 @@ use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageRequest};
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageRequest};
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageRequest};
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageRequest};
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageNumImages, GenerateFlux1DevTextToImageRequest};
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageNumImages, GenerateFlux1SchnellTextToImageRequest};
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageNumImages, GenerateFluxPro11TextToImageRequest};
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageNumImages, GenerateFluxPro11UltraTextToImageRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
@@ -57,8 +57,11 @@ pub async fn handle_image_artcraft(
|
||||
None => {
|
||||
return Err(InternalImageError::NoModelSpecified);
|
||||
}
|
||||
Some(ImageModel::GptImage1) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: artcraft is handling sora images")));
|
||||
Some(
|
||||
ImageModel::GptImage1 |
|
||||
ImageModel::Midjourney
|
||||
) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: another branch should handle this: {:?}", request.model)));
|
||||
}
|
||||
Some(ImageModel::Recraft3) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented in Artcraft")));
|
||||
@@ -66,19 +69,25 @@ pub async fn handle_image_artcraft(
|
||||
Some(ImageModel::Flux1Dev) => {
|
||||
info!("enqueue Flux 1 Dev");
|
||||
selected_model = GenerationModel::Flux1Dev;
|
||||
let aspect_ratio = request.aspect_ratio
|
||||
.map(|aspect| match aspect {
|
||||
// TODO(bt,2025-07-14): Support other aspect ratios.
|
||||
TextToImageSize::Tall => GenerateFlux1DevTextToImageAspectRatio::PortraitNineBySixteen,
|
||||
TextToImageSize::Wide => GenerateFlux1DevTextToImageAspectRatio::LandscapeSixteenByNine,
|
||||
TextToImageSize::Auto => GenerateFlux1DevTextToImageAspectRatio::SquareHd,
|
||||
TextToImageSize::Square => GenerateFlux1DevTextToImageAspectRatio::SquareHd,
|
||||
});
|
||||
let request = GenerateFlux1DevTextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt,
|
||||
aspect_ratio,
|
||||
num_images: None,
|
||||
aspect_ratio: request.aspect_ratio
|
||||
.map(|aspect| match aspect {
|
||||
// TODO(bt,2025-07-14): Support other aspect ratios.
|
||||
TextToImageSize::Tall => GenerateFlux1DevTextToImageAspectRatio::PortraitNineBySixteen,
|
||||
TextToImageSize::Wide => GenerateFlux1DevTextToImageAspectRatio::LandscapeSixteenByNine,
|
||||
TextToImageSize::Auto => GenerateFlux1DevTextToImageAspectRatio::SquareHd,
|
||||
TextToImageSize::Square => GenerateFlux1DevTextToImageAspectRatio::SquareHd,
|
||||
}),
|
||||
num_images: request.number_images
|
||||
.and_then(|num| match num {
|
||||
1 => Some(GenerateFlux1DevTextToImageNumImages::One),
|
||||
2 => Some(GenerateFlux1DevTextToImageNumImages::Two),
|
||||
3 => Some(GenerateFlux1DevTextToImageNumImages::Three),
|
||||
4 => Some(GenerateFlux1DevTextToImageNumImages::Four),
|
||||
_ => None, // NB: use service defaults
|
||||
}),
|
||||
};
|
||||
let result = generate_flux_1_dev_text_to_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
@@ -99,19 +108,25 @@ pub async fn handle_image_artcraft(
|
||||
Some(ImageModel::Flux1Schnell) => {
|
||||
info!("enqueue Flux 1 Schnell");
|
||||
selected_model = GenerationModel::Flux1Schnell;
|
||||
let aspect_ratio = request.aspect_ratio
|
||||
.map(|aspect| match aspect {
|
||||
// TODO(bt,2025-07-14): Support other aspect ratios.
|
||||
TextToImageSize::Tall => GenerateFlux1SchnellTextToImageAspectRatio::PortraitNineBySixteen,
|
||||
TextToImageSize::Wide => GenerateFlux1SchnellTextToImageAspectRatio::LandscapeSixteenByNine,
|
||||
TextToImageSize::Auto => GenerateFlux1SchnellTextToImageAspectRatio::SquareHd,
|
||||
TextToImageSize::Square => GenerateFlux1SchnellTextToImageAspectRatio::SquareHd,
|
||||
});
|
||||
let request = GenerateFlux1SchnellTextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt,
|
||||
aspect_ratio,
|
||||
num_images: None,
|
||||
aspect_ratio: request.aspect_ratio
|
||||
.map(|aspect| match aspect {
|
||||
// TODO(bt,2025-07-14): Support other aspect ratios.
|
||||
TextToImageSize::Tall => GenerateFlux1SchnellTextToImageAspectRatio::PortraitNineBySixteen,
|
||||
TextToImageSize::Wide => GenerateFlux1SchnellTextToImageAspectRatio::LandscapeSixteenByNine,
|
||||
TextToImageSize::Square => GenerateFlux1SchnellTextToImageAspectRatio::SquareHd,
|
||||
TextToImageSize::Auto => GenerateFlux1SchnellTextToImageAspectRatio::SquareHd,
|
||||
}),
|
||||
num_images: request.number_images
|
||||
.and_then(|num| match num {
|
||||
1 => Some(GenerateFlux1SchnellTextToImageNumImages::One),
|
||||
2 => Some(GenerateFlux1SchnellTextToImageNumImages::Two),
|
||||
3 => Some(GenerateFlux1SchnellTextToImageNumImages::Three),
|
||||
4 => Some(GenerateFlux1SchnellTextToImageNumImages::Four),
|
||||
_ => None, // NB: use service defaults
|
||||
}),
|
||||
};
|
||||
let result = generate_flux_1_schnell_text_to_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
@@ -132,19 +147,25 @@ pub async fn handle_image_artcraft(
|
||||
Some(ImageModel::FluxPro11) => {
|
||||
info!("enqueue Flux Pro 1.1");
|
||||
selected_model = GenerationModel::FluxPro11;
|
||||
let aspect_ratio = request.aspect_ratio
|
||||
.map(|aspect| match aspect {
|
||||
// TODO(bt,2025-07-14): Support other aspect ratios.
|
||||
TextToImageSize::Tall => GenerateFluxPro11TextToImageAspectRatio::PortraitNineBySixteen,
|
||||
TextToImageSize::Wide => GenerateFluxPro11TextToImageAspectRatio::LandscapeSixteenByNine,
|
||||
TextToImageSize::Auto => GenerateFluxPro11TextToImageAspectRatio::Square,
|
||||
TextToImageSize::Square => GenerateFluxPro11TextToImageAspectRatio::Square,
|
||||
});
|
||||
let request = GenerateFluxPro11TextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt,
|
||||
aspect_ratio,
|
||||
num_images: None,
|
||||
aspect_ratio: request.aspect_ratio
|
||||
.map(|aspect| match aspect {
|
||||
// TODO(bt,2025-07-14): Support other aspect ratios.
|
||||
TextToImageSize::Tall => GenerateFluxPro11TextToImageAspectRatio::PortraitNineBySixteen,
|
||||
TextToImageSize::Wide => GenerateFluxPro11TextToImageAspectRatio::LandscapeSixteenByNine,
|
||||
TextToImageSize::Square => GenerateFluxPro11TextToImageAspectRatio::Square,
|
||||
TextToImageSize::Auto => GenerateFluxPro11TextToImageAspectRatio::Square,
|
||||
}),
|
||||
num_images: request.number_images
|
||||
.and_then(|num| match num {
|
||||
1 => Some(GenerateFluxPro11TextToImageNumImages::One),
|
||||
2 => Some(GenerateFluxPro11TextToImageNumImages::Two),
|
||||
3 => Some(GenerateFluxPro11TextToImageNumImages::Three),
|
||||
4 => Some(GenerateFluxPro11TextToImageNumImages::Four),
|
||||
_ => None, // NB: use service defaults
|
||||
}),
|
||||
};
|
||||
let result = generate_flux_pro_11_text_to_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
@@ -165,19 +186,25 @@ pub async fn handle_image_artcraft(
|
||||
Some(ImageModel::FluxPro11Ultra) => {
|
||||
info!("enqueue Flux Pro 1.1 Ultra");
|
||||
selected_model = GenerationModel::FluxPro11Ultra;
|
||||
let aspect_ratio = request.aspect_ratio
|
||||
.map(|aspect| match aspect {
|
||||
// TODO(bt,2025-07-14): Support other aspect ratios.
|
||||
TextToImageSize::Tall => GenerateFluxPro11UltraTextToImageAspectRatio::PortraitNineBySixteen,
|
||||
TextToImageSize::Wide => GenerateFluxPro11UltraTextToImageAspectRatio::LandscapeSixteenByNine,
|
||||
TextToImageSize::Auto => GenerateFluxPro11UltraTextToImageAspectRatio::Square,
|
||||
TextToImageSize::Square => GenerateFluxPro11UltraTextToImageAspectRatio::Square,
|
||||
});
|
||||
let request = GenerateFluxPro11UltraTextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt,
|
||||
aspect_ratio,
|
||||
num_images: None,
|
||||
aspect_ratio: request.aspect_ratio
|
||||
.map(|aspect| match aspect {
|
||||
// TODO(bt,2025-07-14): Support other aspect ratios.
|
||||
TextToImageSize::Tall => GenerateFluxPro11UltraTextToImageAspectRatio::PortraitNineBySixteen,
|
||||
TextToImageSize::Wide => GenerateFluxPro11UltraTextToImageAspectRatio::LandscapeSixteenByNine,
|
||||
TextToImageSize::Square => GenerateFluxPro11UltraTextToImageAspectRatio::Square,
|
||||
TextToImageSize::Auto => GenerateFluxPro11UltraTextToImageAspectRatio::Square,
|
||||
}),
|
||||
num_images: request.number_images
|
||||
.and_then(|num| match num {
|
||||
1 => Some(GenerateFluxPro11UltraTextToImageNumImages::One),
|
||||
2 => Some(GenerateFluxPro11UltraTextToImageNumImages::Two),
|
||||
3 => Some(GenerateFluxPro11UltraTextToImageNumImages::Three),
|
||||
4 => Some(GenerateFluxPro11UltraTextToImageNumImages::Four),
|
||||
_ => None, // NB: use service defaults
|
||||
}),
|
||||
};
|
||||
let result = generate_flux_pro_11_ultra_text_to_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
|
||||
@@ -51,8 +51,11 @@ pub async fn handle_image_fal(
|
||||
None => {
|
||||
return Err(InternalImageError::NoModelSpecified);
|
||||
}
|
||||
Some(ImageModel::GptImage1) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: fal is handling sora images")));
|
||||
Some(
|
||||
ImageModel::GptImage1 |
|
||||
ImageModel::Midjourney
|
||||
) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: another branch should handle this: {:?}", request.model)));
|
||||
}
|
||||
Some(
|
||||
ImageModel::Flux1Dev |
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
|
||||
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use log::{error, info};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use std::time::Duration;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_gpt_image_1(
|
||||
request: EnqueueTextToImageRequest,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, InternalImageError> {
|
||||
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
// TODO: Check if providers are available before proceeding.
|
||||
|
||||
info!("Providers by priority: {:?}", priority);
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Fal => {
|
||||
// Fallthrough
|
||||
// If in the future we support OpenAI API keys, it's worth considering whether we
|
||||
// send those requests ourselves, or if we use FAL as an intermediary. FAL makes
|
||||
// the API nicer to deal with, but the user needs an additional key.
|
||||
}
|
||||
Provider::Artcraft => {
|
||||
info!("Dispatching gpt-image-1 via Artcraft...");
|
||||
return handle_gpt_image_1_artcraft(
|
||||
request,
|
||||
app_env_configs,
|
||||
storyteller_creds_manager
|
||||
).await;
|
||||
}
|
||||
Provider::Sora => {
|
||||
info!("Dispatching gpt-image-1 via Sora...");
|
||||
return handle_gpt_image_1_sora(
|
||||
request,
|
||||
app,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(InternalImageError::NoProviderAvailable)
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize};
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use std::time::Duration;
|
||||
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
|
||||
use storyteller_client::generate::image::generate_gpt_image_1_text_to_image::generate_gpt_image_1_text_to_image;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_gpt_image_1_artcraft(
|
||||
request: EnqueueTextToImageRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, InternalImageError> {
|
||||
|
||||
let creds = match storyteller_creds_manager.get_credentials()? {
|
||||
Some(creds) => creds,
|
||||
None => {
|
||||
return Err(InternalImageError::NeedsStorytellerCredentials);
|
||||
},
|
||||
};
|
||||
|
||||
info!("Calling Artcraft gpt-image-1 ...");
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let image_quality = Some(GenerateGptImage1TextToImageImageQuality::High);
|
||||
|
||||
//let image_quality = request.image_quality
|
||||
// .map(|quality| match quality {
|
||||
// EditImageQuality::Auto => GenerateGptImage1TextToImageImageQuality::Auto,
|
||||
// EditImageQuality::High => GenerateGptImage1TextToImageImageQuality::High,
|
||||
// EditImageQuality::Medium => GenerateGptImage1TextToImageImageQuality::Medium,
|
||||
// EditImageQuality::Low => GenerateGptImage1TextToImageImageQuality::Low,
|
||||
// });
|
||||
|
||||
let image_size = request.aspect_ratio
|
||||
.map(|size| match size {
|
||||
TextToImageSize::Auto => GenerateGptImage1TextToImageImageSize::Square,
|
||||
TextToImageSize::Square => GenerateGptImage1TextToImageImageSize::Square,
|
||||
TextToImageSize::Tall => GenerateGptImage1TextToImageImageSize::Vertical,
|
||||
TextToImageSize::Wide => GenerateGptImage1TextToImageImageSize::Horizontal,
|
||||
});
|
||||
|
||||
let num_images = match request.number_images {
|
||||
None => None,
|
||||
Some(1) => Some(GenerateGptImage1TextToImageNumImages::One),
|
||||
Some(2) => Some(GenerateGptImage1TextToImageNumImages::Two),
|
||||
Some(3) => Some(GenerateGptImage1TextToImageNumImages::Three),
|
||||
Some(4) => Some(GenerateGptImage1TextToImageNumImages::Four),
|
||||
// TODO: Error
|
||||
//Some(other) => {
|
||||
// return Err(InternalImageError::InvalidNumberOfRequestedImages {
|
||||
// min: 1,
|
||||
// max: 4,
|
||||
// requested: other,
|
||||
// });
|
||||
//},
|
||||
_ => Some(GenerateGptImage1TextToImageNumImages::One), // Default to one image if invalid number
|
||||
};
|
||||
|
||||
let request = GenerateGptImage1TextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt.clone(),
|
||||
image_size,
|
||||
num_images,
|
||||
image_quality,
|
||||
};
|
||||
|
||||
let result = generate_gpt_image_1_text_to_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
|
||||
let job_id = match result {
|
||||
Ok(enqueued) => {
|
||||
// TODO(bt,2025-07-05): Enqueue job token?
|
||||
info!("Successfully enqueued Artcraft gpt-image-1. Job token: {}",
|
||||
enqueued.inference_job_token);
|
||||
enqueued.inference_job_token
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
|
||||
return Err(InternalImageError::StorytellerError(err));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
provider: GenerationProvider::Artcraft,
|
||||
model: Some(GenerationModel::GptImage1),
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
})
|
||||
}
|
||||
+12
-3
@@ -19,9 +19,9 @@ use tauri::AppHandle;
|
||||
|
||||
const SORA_SIMPLE_IMAGE_GEN_TIMEOUT : Duration = Duration::from_millis(1000 * 30); // 30 seconds
|
||||
|
||||
pub async fn handle_image_sora(
|
||||
app: &AppHandle,
|
||||
pub async fn handle_gpt_image_1_sora(
|
||||
request: EnqueueTextToImageRequest,
|
||||
app: &AppHandle,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, InternalImageError> {
|
||||
@@ -55,10 +55,19 @@ pub async fn handle_image_sora(
|
||||
Some(TextToImageSize::Wide) => ImageSize::Wide,
|
||||
};
|
||||
|
||||
let num_images = match request.number_images {
|
||||
None => NumImages::One,
|
||||
Some(1) => NumImages::One,
|
||||
Some(2) => NumImages::Two,
|
||||
Some(3) => NumImages::Two, // NB: There is no option for three images!
|
||||
Some(4) => NumImages::Four,
|
||||
_ => NumImages::One,
|
||||
};
|
||||
|
||||
let result =
|
||||
simple_image_gen_with_session_auto_renew(SimpleImageGenAutoRenewRequest {
|
||||
prompt: prompt.to_string(),
|
||||
num_images: NumImages::One,
|
||||
num_images,
|
||||
image_size: size,
|
||||
credentials: &creds,
|
||||
request_timeout: Some(SORA_SIMPLE_IMAGE_GEN_TIMEOUT),
|
||||
@@ -1 +1,3 @@
|
||||
pub mod handle_image_sora;
|
||||
pub (crate) mod handle_gpt_image_1_artcraft;
|
||||
pub (crate) mod handle_gpt_image_1_sora;
|
||||
pub (super) mod handle_gpt_image_1;
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::core::artcraft_error::ArtcraftError;
|
||||
use base64::DecodeError;
|
||||
use errors::AnyhowError;
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use midjourney_client::error::midjourney_error::MidjourneyError;
|
||||
use openai_sora_client::sora_error::SoraError;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
|
||||
@@ -10,9 +11,11 @@ pub enum InternalImageError {
|
||||
NoModelSpecified,
|
||||
NoProviderAvailable,
|
||||
NeedsFalApiKey,
|
||||
NeedsMidjourneyCredentials,
|
||||
NeedsSoraCredentials,
|
||||
NeedsStorytellerCredentials,
|
||||
FalError(FalErrorPlus),
|
||||
MidjourneyError(MidjourneyError),
|
||||
SoraError(SoraError),
|
||||
AnyhowError(AnyhowError),
|
||||
StorytellerError(StorytellerError),
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize};
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use midjourney_client::client::midjourney_hostname::MidjourneyHostname;
|
||||
use midjourney_client::endpoints::submit_job::{submit_job, SubmitJobRequest};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use std::time::Duration;
|
||||
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
|
||||
use storyteller_client::generate::image::generate_gpt_image_1_text_to_image::generate_gpt_image_1_text_to_image;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_midjourney(
|
||||
request: EnqueueTextToImageRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
mj_creds_manager: &MidjourneyCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, InternalImageError> {
|
||||
|
||||
let creds = match mj_creds_manager.maybe_copy_cookie_store() {
|
||||
Ok(Some(creds)) => creds,
|
||||
Ok(None) => {
|
||||
return Err(InternalImageError::NeedsMidjourneyCredentials);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error reading Midjourney credentials: {:?}", err);
|
||||
return Err(InternalImageError::NeedsMidjourneyCredentials);
|
||||
},
|
||||
};
|
||||
|
||||
info!("Calling midjourney ...");
|
||||
|
||||
let cookie_header = creds.to_cookie_string();
|
||||
|
||||
let prompt = request.prompt
|
||||
.as_deref()
|
||||
.unwrap_or("");
|
||||
|
||||
|
||||
let result = submit_job(SubmitJobRequest {
|
||||
prompt,
|
||||
channel_id: "singleplayer_f8a57ac3-e416-4dd4-9be8-2c4223691b01", // TODO: DO NOT COMMIT
|
||||
hostname: MidjourneyHostname::Standard,
|
||||
cookie_header,
|
||||
}).await;
|
||||
|
||||
let job_id = match result {
|
||||
Ok(result) => {
|
||||
// TODO(bt,2025-07-05): Enqueue job token?
|
||||
info!("Successfully enqueued MidJourney. Job token: {}",result.job_id);
|
||||
result.job_id
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to use MidJourney: {:?}", err);
|
||||
return Err(InternalImageError::MidjourneyError(err));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
provider: GenerationProvider::Midjourney,
|
||||
model: Some(GenerationModel::Midjourney),
|
||||
provider_job_id: Some(job_id),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub (super) mod handle_midjourney;
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod enqueue_text_to_image_command;
|
||||
pub mod generic;
|
||||
pub mod internal_image_error;
|
||||
mod gpt_image_1;
|
||||
mod gpt_image_1;
|
||||
mod midjourney;
|
||||
+23
-2
@@ -3,7 +3,7 @@ use crate::core::commands::enqueue::image::internal_image_error::InternalImageEr
|
||||
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
|
||||
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal::handle_generic_bg_removal;
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::handle_gpt_image_1;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
|
||||
@@ -43,6 +43,7 @@ use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use storyteller_client::media_files::get_media_file::get_media_file;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -55,6 +56,21 @@ pub struct EnqueueImageBgRemovalCommand {
|
||||
|
||||
/// Base64-encoded image
|
||||
pub base64_image: Option<String>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Name of the frontend caller.
|
||||
/// We'll use this to selectively trigger events.
|
||||
pub frontend_caller: Option<TauriCommandCaller>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined identifier that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined payload that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_payload: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -169,7 +185,12 @@ pub async fn handle_request(
|
||||
).await?;
|
||||
|
||||
let result = success_event
|
||||
.insert_into_task_database(task_database)
|
||||
.insert_into_task_database_with_frontend_payload(
|
||||
task_database,
|
||||
request.frontend_caller,
|
||||
request.frontend_subscriber_id.as_deref(),
|
||||
request.frontend_subscriber_payload.as_deref()
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(err) = result {
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@ use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg
|
||||
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal_fal::handle_generic_bg_removal_fal;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
|
||||
+8
-4
@@ -39,7 +39,7 @@ use storyteller_client::generate::image::generate_flux_1_schnell_text_to_image::
|
||||
use storyteller_client::generate::image::generate_flux_pro_11_text_to_image::generate_flux_pro_11_text_to_image;
|
||||
use storyteller_client::generate::image::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
|
||||
use storyteller_client::generate::image::remove_image_background::remove_image_background;
|
||||
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
|
||||
use storyteller_client::media_files::upload_image_media_file_from_file::{upload_image_media_file_from_file, UploadImageFromFileArgs};
|
||||
use tauri::AppHandle;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
@@ -123,9 +123,13 @@ async fn upload_image_from_base64_bytes(
|
||||
|
||||
info!("Uploading image media file from temp file: {:?}", temp_file.path());
|
||||
|
||||
let result =
|
||||
upload_image_media_file_from_file(&app_env_configs.storyteller_host, Some(&creds), temp_file)
|
||||
.await
|
||||
let result =
|
||||
upload_image_media_file_from_file(UploadImageFromFileArgs {
|
||||
api_host: &app_env_configs.storyteller_host,
|
||||
maybe_creds: Some(&creds),
|
||||
path: temp_file,
|
||||
is_intermediate_system_file: true, // NB: Probably not essential to keep this.
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upload image media file: {:?}", err);
|
||||
InternalBgRemovalError::StorytellerError(err)
|
||||
|
||||
+17
-3
@@ -1,7 +1,7 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, EnqueueTextToImageSuccessResponse};
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::handle_gpt_image_1;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
|
||||
@@ -43,6 +43,7 @@ use storyteller_client::media_files::get_media_file::get_media_file;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use crate::core::commands::enqueue::image_edit::flux_kontext::handle_flux_kontext_edit::handle_flux_kontext_edit;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct EnqueueContextualEditImageCommand {
|
||||
@@ -207,8 +208,22 @@ pub async fn handle_request(
|
||||
None => {
|
||||
return Err(InternalContextualEditImageError::NoModelSpecified)
|
||||
}
|
||||
Some(ContextualImageEditModel::FluxProKontextMax) => {
|
||||
handle_flux_kontext_edit(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await?
|
||||
}
|
||||
Some(ContextualImageEditModel::GptImage1) => {
|
||||
handle_gpt_image_1(
|
||||
handle_gpt_image_1_edit(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
@@ -221,7 +236,6 @@ pub async fn handle_request(
|
||||
sora_task_queue,
|
||||
).await?
|
||||
}
|
||||
// TODO(bt,2025-07-05): Flux Kontext, etc.
|
||||
};
|
||||
|
||||
let result = success_event
|
||||
|
||||
@@ -30,6 +30,12 @@ pub enum InternalContextualEditImageError {
|
||||
requested: u32,
|
||||
},
|
||||
|
||||
// TODO: Maybe just reuse the other error enum variant
|
||||
/// An invalid number of input images was requested for Flux Kontext.
|
||||
InvalidNumberOfInputImagesForFluxKontext {
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// No sora credentials are set.
|
||||
NeedsSoraCredentials,
|
||||
|
||||
@@ -84,6 +90,11 @@ impl InternalContextualEditImageError {
|
||||
error_type = EnqueueContextualEditImageErrorType::BadRequest;
|
||||
error_message = format!("Invalid number of input images ({}). Must be between {} and {}", requested, min, max);
|
||||
}
|
||||
InternalContextualEditImageError::InvalidNumberOfInputImagesForFluxKontext { message } => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueContextualEditImageErrorType::BadRequest;
|
||||
error_message = message.clone();
|
||||
}
|
||||
InternalContextualEditImageError::SoraError(_) => {}
|
||||
InternalContextualEditImageError::AnyhowError(_) => {}
|
||||
InternalContextualEditImageError::StorytellerError(_) => {}
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::flux_kontext::handle_flux_kontext_edit_artcraft::handle_flux_kontext_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use log::info;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_flux_kontext_edit(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
|
||||
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
// TODO: Check if providers are available before proceeding.
|
||||
|
||||
info!("Providers by priority: {:?}", priority);
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Fal => {
|
||||
// Fallthrough (TODO)
|
||||
}
|
||||
Provider::Sora => {
|
||||
// Fallthrough
|
||||
}
|
||||
Provider::Artcraft => {
|
||||
info!("Dispatching gpt-image-1 (edit) via Artcraft...");
|
||||
return handle_flux_kontext_edit_artcraft(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
storyteller_creds_manager
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(InternalContextualEditImageError::NoProviderAvailable)
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{FluxProKontextMaxEditImageNumImages, FluxProKontextMaxEditImageRequest};
|
||||
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use fal_client::requests::webhook::image::edit::enqueue_flux_pro_kontext_max_edit_webhook::FluxProKontextMaxNumImages;
|
||||
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::GptEditImageQuality;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use storyteller_client::generate::image::edit::flux_pro_kontext_max_edit_image::flux_pro_kontext_max_edit_image;
|
||||
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
|
||||
use storyteller_client::generate::image::generate_flux_1_dev_text_to_image::generate_flux_1_dev_text_to_image;
|
||||
use storyteller_client::generate::image::generate_flux_1_schnell_text_to_image::generate_flux_1_schnell_text_to_image;
|
||||
use storyteller_client::generate::image::generate_flux_pro_11_text_to_image::generate_flux_pro_11_text_to_image;
|
||||
use storyteller_client::generate::image::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_flux_kontext_edit_artcraft(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
|
||||
|
||||
let creds = match storyteller_creds_manager.get_credentials()? {
|
||||
Some(creds) => creds,
|
||||
None => {
|
||||
return Err(InternalContextualEditImageError::NeedsStorytellerCredentials);
|
||||
},
|
||||
};
|
||||
|
||||
info!("Calling Artcraft flux kontext max edit (edit) ...");
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let num_images = match request.image_count {
|
||||
None => None,
|
||||
Some(1) => Some(FluxProKontextMaxEditImageNumImages::One),
|
||||
Some(2) => Some(FluxProKontextMaxEditImageNumImages::Two),
|
||||
Some(3) => Some(FluxProKontextMaxEditImageNumImages::Three),
|
||||
Some(4) => Some(FluxProKontextMaxEditImageNumImages::Four),
|
||||
Some(other) => {
|
||||
return Err(InternalContextualEditImageError::InvalidNumberOfRequestedImages {
|
||||
min: 1,
|
||||
max: 4,
|
||||
requested: other,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
let mut maybe_media_token = None;
|
||||
|
||||
let list_count = request.image_media_tokens
|
||||
.as_ref()
|
||||
.map(|tokens| tokens.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
if request.scene_image_media_token.is_some() && list_count > 0 {
|
||||
return Err(InternalContextualEditImageError::InvalidNumberOfInputImagesForFluxKontext {
|
||||
message: "Cannot specify both scene_image_media_token and image_media_tokens for Flux Kontext Max".to_string()
|
||||
});
|
||||
}
|
||||
|
||||
if list_count > 1 {
|
||||
return Err(InternalContextualEditImageError::InvalidNumberOfInputImagesForFluxKontext {
|
||||
message: "Cannot specify more than one image in image_media_tokens for Flux Kontext Max".to_string()
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(media_token) = &request.scene_image_media_token {
|
||||
maybe_media_token = Some(media_token.clone());
|
||||
}
|
||||
|
||||
let maybe_list_media_token = request.image_media_tokens
|
||||
.as_ref()
|
||||
.map(|tokens| tokens.first())
|
||||
.flatten();
|
||||
|
||||
if let Some(media_token) = maybe_list_media_token {
|
||||
maybe_media_token = Some(media_token.clone());
|
||||
}
|
||||
|
||||
let media_token = match maybe_media_token {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
return Err(InternalContextualEditImageError::InvalidNumberOfInputImagesForFluxKontext {
|
||||
message: "No input image specified for Flux Kontext Max edit".to_string(),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
let request = FluxProKontextMaxEditImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: Some(request.prompt.clone()),
|
||||
image_media_token: media_token,
|
||||
num_images,
|
||||
};
|
||||
|
||||
let result = flux_pro_kontext_max_edit_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
|
||||
let job_id = match result {
|
||||
Ok(enqueued) => {
|
||||
// TODO(bt,2025-07-05): Enqueue job token?
|
||||
info!("Successfully enqueued Artcraft gpt-image-1. Job token: {}",
|
||||
enqueued.inference_job_token);
|
||||
enqueued.inference_job_token
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
|
||||
return Err(InternalContextualEditImageError::StorytellerError(err));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
provider: GenerationProvider::Artcraft,
|
||||
model: Some(GenerationModel::FluxProKontextMax),
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub (super) mod handle_flux_kontext_edit;
|
||||
mod handle_flux_kontext_edit_artcraft;
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
@@ -16,7 +16,7 @@ use tauri::AppHandle;
|
||||
|
||||
pub(super) const MAX_IMAGES: usize = 10;
|
||||
|
||||
pub async fn handle_gpt_image_1(
|
||||
pub async fn handle_gpt_image_1_edit(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
@@ -44,8 +44,8 @@ pub async fn handle_gpt_image_1(
|
||||
// the API nicer to deal with, but the user needs an additional key.
|
||||
}
|
||||
Provider::Artcraft => {
|
||||
info!("Dispatching gpt-image-1 via Artcraft...");
|
||||
return handle_gpt_image_1_artcraft(
|
||||
info!("Dispatching gpt-image-1 (edit) via Artcraft...");
|
||||
return handle_gpt_image_1_edit_artcraft(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
@@ -54,8 +54,8 @@ pub async fn handle_gpt_image_1(
|
||||
).await;
|
||||
}
|
||||
Provider::Sora => {
|
||||
info!("Dispatching gpt-image-1 via Sora...");
|
||||
return handle_gpt_image_1_sora(
|
||||
info!("Dispatching gpt-image-1 (edit) via Sora...");
|
||||
return handle_gpt_image_1_edit_sora(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
+3
-3
@@ -2,7 +2,7 @@ use crate::core::commands::enqueue::image::enqueue_text_to_image_command::Enqueu
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::MAX_IMAGES;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::MAX_IMAGES;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
@@ -36,7 +36,7 @@ use tauri::AppHandle;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
|
||||
pub async fn handle_gpt_image_1_artcraft(
|
||||
pub async fn handle_gpt_image_1_edit_artcraft(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
@@ -51,7 +51,7 @@ pub async fn handle_gpt_image_1_artcraft(
|
||||
},
|
||||
};
|
||||
|
||||
info!("Calling Artcraft gpt-image-1 ...");
|
||||
info!("Calling Artcraft gpt-image-1 (edit) ...");
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ use crate::core::commands::enqueue::image::enqueue_text_to_image_command::Enqueu
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageSize, EnqueueContextualEditImageCommand, EnqueueContextualEditImageErrorType};
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::MAX_IMAGES;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::MAX_IMAGES;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
@@ -48,7 +48,7 @@ const SORA_IMAGE_REMIX_TIMEOUT: Duration = Duration::from_millis(1000 * 30); //
|
||||
|
||||
const DEFAULT_ASPECT_RATIO : EditImageSize = EditImageSize::Square;
|
||||
|
||||
pub async fn handle_gpt_image_1_sora(
|
||||
pub async fn handle_gpt_image_1_edit_sora(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod handle_gpt_image_1;
|
||||
pub mod handle_gpt_image_1_artcraft;
|
||||
pub mod handle_gpt_image_1_sora;
|
||||
pub mod handle_gpt_image_1_edit;
|
||||
pub mod handle_gpt_image_1_edit_artcraft;
|
||||
pub mod handle_gpt_image_1_edit_sora;
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod enqueue_contextual_edit_image_command;
|
||||
pub mod errors;
|
||||
pub mod gpt_image_1;
|
||||
pub mod gpt_image_1;
|
||||
mod flux_kontext;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user