Compare commits

...
Author SHA1 Message Date
Brandon Thomas a0058dc28c plumb controls to backend 2025-07-29 21:14:01 -04:00
bflatastic af02cc6525 number of generation dropdown 2025-07-29 20:18:55 -04:00
Brandon Thomas 4f0916edb7 enqueue multiple images (#749) 2025-07-29 20:14:10 -04:00
bflatastic c36e88d8ce expandable lightbox to fullscreen 2025-07-29 20:11:04 +07:00
Brandon Thomas 233f60acf6 stop double alerting on events 2025-07-28 22:50:02 -04:00
Brandon Thomas a3e16e491a unlikely we'll need old frontend configuration "editor2d_studio_OAI" 2025-07-28 20:04:27 -04:00
Brandon Thomas 3d521fec79 roll forward with new build (07.28) 2025-07-28 10:16:14 -04:00
BFlat 6c8979123f 3d: fixed saving and new scene (#747) 2025-07-28 14:12:56 +07:00
BFlat 8d4edcfafd lightbox context images, generation details, canvas snapshot bug fix (#746)
- canvas snapshot aspect ratio is fixed (it was snapshotting only as
squared images)
- added reference/context images in the lightbox under prompt, hovering
shows a larger preview
- added generation details: model used with logo and provider
2025-07-28 13:28:17 +07:00
Brandon Thomas b53e138dab include prompt details in api docs 2025-07-26 19:10:48 -04:00
Brandon Thomas dc1b038ffa only serve image references for prompts (future proofing) 2025-07-26 18:59:08 -04:00
Brandon Thomas 418a9f92c0 insert prompt images, serve additional metadata (#745) 2025-07-26 18:53:09 -04:00
Brandon Thomas d671f7f951 record prompts on all generate/* andpoints (#744) 2025-07-25 18:29:44 -04:00
bflatastic e97e5c239e lightbox and download button fixes 2025-07-24 17:23:10 +07:00
bflatastic a7270f4685 lightbox prompt fetching 2025-07-24 16:21:21 +07:00
Brandon Thomas a6fb661fbe codegen db 2025-07-24 02:15:22 -04:00
Brandon Thomas e5f5a06b6b prompts code (#743) 2025-07-24 02:01:34 -04:00
Brandon Thomas 48a6b1a046 more migrations (#742) 2025-07-23 17:35:10 -04:00
Brandon Thomas 7791b9b6cc Bt prompts schema (#741) 2025-07-23 16:22:13 -04:00
Brandon Thomas ff30213af8 more GA instrumentation (#740) 2025-07-22 21:12:52 -04:00
Brandon Thomas 766f726dc1 Google Analytics package and events (#739) 2025-07-22 19:12:14 -04:00
Brandon Thomas 76d7d4c100 add microsoft clarity (#738) 2025-07-22 10:58:56 -04:00
BFlat 2902db074e Upload asset ux update (#737)
- image upload modal added
- everything under + button popover
<img width="393" height="263" alt="image"
src="https://github.com/user-attachments/assets/9f6bda8d-10c0-460f-8eab-227ad8155e68"
/>
2025-07-22 20:25:07 +07:00
Brandon Thomas b8ee03f1b7 load glb assets without cors (#736) 2025-07-22 08:43:38 -04:00
Brandon Thomas 2cf300588e inference job query reaper (#735) 2025-07-21 19:42:27 -04:00
BFlat d2720d0a62 services icons for the models list (#733)
added model service icons:
- google
- black forest labs
- bytedance
- kling
- midjourney
- openai
2025-07-21 14:51:15 +07:00
BFlat 69ed3ec48b bug fix: added z index to scene state (#734)
layers z-index now are persisted in scene state
2025-07-21 14:49:23 +07:00
bflatastic 81f5797e2f removed download link from completed card 2025-07-21 13:28:45 +07:00
Brandon Thomas a80c69ab1e update download links (#732) 2025-07-17 22:05:56 -04:00
714 changed files with 4728 additions and 47083 deletions
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE prompts DROP COLUMN maybe_anonymous_visitor_token;
@@ -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;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs DROP COLUMN maybe_prompt_token;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
ADD COLUMN maybe_prompt_token VARCHAR(32) DEFAULT NULL;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE media_files DROP COLUMN maybe_generation_provider;
@@ -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,
+3
View File
@@ -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.
@@ -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;
@@ -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)
@@ -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(())
}
}
@@ -3,4 +3,5 @@ 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 remove_background_rembg_webhook;
@@ -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?)
}
@@ -3,4 +3,5 @@ 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 remove_image_background;
@@ -1,6 +1,7 @@
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::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
@@ -39,6 +40,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 -
@@ -224,7 +228,16 @@ 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;
}
_ => {
// Fall-through
@@ -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};
@@ -58,7 +58,7 @@ pub async fn handle_image_artcraft(
return Err(InternalImageError::NoModelSpecified);
}
Some(ImageModel::GptImage1) => {
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: artcraft is handling sora images")));
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: an earlier branch should have handled gpt-image-1 images for Artcraft")));
}
Some(ImageModel::Recraft3) => {
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented in Artcraft")));
@@ -66,19 +66,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 +105,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 +144,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 +183,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,
@@ -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)
}
@@ -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,
})
}
@@ -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;
@@ -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};
@@ -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;
@@ -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};
@@ -208,7 +208,7 @@ pub async fn handle_request(
return Err(InternalContextualEditImageError::NoModelSpecified)
}
Some(ContextualImageEditModel::GptImage1) => {
handle_gpt_image_1(
handle_gpt_image_1_edit(
request,
app,
app_data_root,
@@ -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,
@@ -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,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;
@@ -0,0 +1,43 @@
use futures::TryFutureExt;
use log::{error, info};
use reqwest::Client;
use tauri::ipc::Response;
/// Request a URL and load it without worrying about CORS.
#[tauri::command]
pub async fn load_without_cors_command(url: String) -> Result<Response, String> {
info!("load_without_cors_command for URL: {}", url);
// TODO(bt,2025-07-22): Make sure the URL is valid and safe to fetch. Check an allowlist.
// TODO(bt,2025-07-22): Should we cache clients so we don't need to SSL handshake every time?
let client = Client::builder()
.gzip(true)
.build()
.map_err(|e| {
error!("Failed to create HTTP client: {:?}", e);
format!("Failed to create HTTP client: {:?}", e)
})?;
let response = client.get(url.clone())
.send()
.await
.map_err(|e| {
error!("Failed to send HTTP request: {:?}", e);
format!("Failed to send HTTP request: {:?}", e)
})?;
if response.status() != 200 {
return Err(format!("Failed to fetch URL: {}. Status: {}", url, response.status()));
}
let bytes = response.bytes()
.await
.map_err(|e| {
error!("Failed to read response bytes: {:?}", e);
format!("Failed to read response bytes: {:?}", e)
})?;
Ok(Response::new(bytes.to_vec()))
}
@@ -2,6 +2,7 @@ pub mod app_preferences;
pub mod enqueue;
pub mod flip_image;
pub mod get_app_info_command;
pub mod load_without_cors_command;
pub mod platform_info_command;
pub mod providers;
pub mod response;
+2
View File
@@ -12,6 +12,7 @@ use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::enqueue_image_to_video_command;
use crate::core::commands::flip_image::flip_image;
use crate::core::commands::get_app_info_command::get_app_info_command;
use crate::core::commands::load_without_cors_command::load_without_cors_command;
use crate::core::commands::platform_info_command::platform_info_command;
use crate::core::commands::providers::get_provider_order_command::get_provider_order_command;
use crate::core::commands::providers::set_provider_order_command::set_provider_order_command;
@@ -154,6 +155,7 @@ pub fn run() {
get_app_preferences_command,
get_fal_api_key_command,
get_provider_order_command,
load_without_cors_command,
open_sora_login_command,
platform_info_command,
set_fal_api_key_command,
+1 -1
View File
@@ -26,7 +26,7 @@ errors = { workspace = true}
mimetypes = { workspace = true }
# External
image = "0.25.6"
image = { version = "0.25.6" , features = ["jpeg", "png", "tiff", "webp"] }
[dev-dependencies]
anyhow = { workspace = true }
@@ -0,0 +1 @@
pub mod webp_bytes_to_png_bytes;
@@ -0,0 +1,21 @@
use crate::error::ImagesError;
use image::{ImageFormat, ImageReader};
use std::io::Cursor;
pub fn webp_bytes_to_png_bytes(
webp_bytes: &[u8],
) -> Result<Vec<u8>, ImagesError> {
let mut reader = ImageReader::new(Cursor::new(webp_bytes));
reader.set_format(ImageFormat::WebP); // Default to WebP format (as fallback).
let image = reader
.with_guessed_format()? // NB: Can raise IoError.
.decode()?;
let mut output_bytes: Vec<u8> = Vec::new();
image.write_to(&mut Cursor::new(&mut output_bytes), ImageFormat::Png)?;
Ok(output_bytes)
}
+31
View File
@@ -0,0 +1,31 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum ImagesError {
IoError(std::io::Error),
ImageError(image::ImageError),
}
impl Error for ImagesError {}
impl Display for ImagesError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ImagesError::IoError(err) => write!(f, "IO Error: {}", err),
ImagesError::ImageError(err) => write!(f, "Image Error: {}", err),
}
}
}
impl From<std::io::Error> for ImagesError {
fn from(err: std::io::Error) -> Self {
ImagesError::IoError(err)
}
}
impl From<image::ImageError> for ImagesError {
fn from(err: image::ImageError) -> Self {
ImagesError::ImageError(err)
}
}
+2
View File
@@ -23,6 +23,8 @@
// Re-export image to everyone importing this library
pub use image;
pub mod encoding;
pub mod error;
pub mod image_info;
pub mod resize_image_file_preserving_aspect;
pub mod resize_preserving_aspect;
@@ -1,3 +1,4 @@
use log::{debug, warn};
use crate::payloads::generic_inference_args::inner_payloads::f5_tts_payload::F5TTSPayload;
use crate::payloads::generic_inference_args::inner_payloads::face_fusion_payload::FaceFusionPayload;
use crate::payloads::generic_inference_args::inner_payloads::gptsovits_payload::GptSovitsPayload;
@@ -180,6 +181,19 @@ impl GenericInferenceArgs {
Ok(serde_json::from_str(json)?)
}
pub fn from_json_optional(json: Option<&str>) -> AnyhowResult<Option<Self>> {
match json {
None => Ok(None),
Some("" | "null" | "NULL" | "undefined" | "UNDEFINED") => {
// NB: Something inserted a few of these records (only in local dev).
// It's better not to stall the job queue than to assert correctness.
warn!("Received empty or null JSON `{json:?}` for GenericInferenceArgs, returning None.");
Ok(None)
},
Some(json_str) => Ok(Some(Self::from_json(json_str)?)),
}
}
pub fn to_json(&self) -> AnyhowResult<String> {
Ok(serde_json::to_string(self)?)
}
@@ -13,6 +13,7 @@ use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
use crate::helpers::boolean_converters::i8_to_bool;
@@ -30,11 +31,35 @@ pub struct FalJobDetails {
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
pub creator_ip_address: String,
pub maybe_prompt_token: Option<PromptToken>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Default)]
struct RawJobRecord {
job_token: InferenceJobToken,
status: JobStatusPlus,
// NB: Nullable, but required to be filled for this query
external_third_party: Option<InferenceJobExternalThirdParty>,
// NB: Nullable, but required to be filled for this query
external_third_party_id: Option<String>,
maybe_creator_user_token: Option<UserToken>,
maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
creator_ip_address: String,
maybe_prompt_token: Option<PromptToken>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
/// Returns Ok(None) when the record cannot be found.
pub async fn get_inference_job_by_fal_id(fal_id: &str, mysql_pool: &MySqlPool)
-> AnyhowResult<Option<FalJobDetails>>
@@ -66,6 +91,8 @@ SELECT
jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,
jobs.creator_ip_address,
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
jobs.created_at,
jobs.updated_at
@@ -106,28 +133,8 @@ fn raw_record_to_public_result(record: RawJobRecord) -> AnyhowResult<FalJobDetai
maybe_creator_user_token: record.maybe_creator_user_token,
maybe_creator_anonymous_visitor_token: record.maybe_creator_anonymous_visitor_token,
creator_ip_address: record.creator_ip_address,
maybe_prompt_token: record.maybe_prompt_token,
created_at: record.created_at,
updated_at: record.updated_at,
})
}
#[derive(Debug, Default)]
struct RawJobRecord {
pub job_token: InferenceJobToken,
pub status: JobStatusPlus,
// NB: Nullable, but required to be filled for this query
pub external_third_party: Option<InferenceJobExternalThirdParty>,
// NB: Nullable, but required to be filled for this query
pub external_third_party_id: Option<String>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
pub creator_ip_address: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -1,6 +1,8 @@
use anyhow::anyhow;
use log::info;
use sqlx::MySqlPool;
use sqlx::{Executor, MySql};
use std::marker::PhantomData;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::by_table::generic_inference_jobs::inference_input_source_token_type::InferenceInputSourceTokenType;
@@ -13,6 +15,7 @@ use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
use crate::errors::database_query_error::DatabaseQueryError;
@@ -27,26 +30,35 @@ pub enum FalCategory {
ObjectGeneration,
}
pub struct InsertGenericInferenceForFalArgs<'a> {
pub uuid_idempotency_token: &'a str,
pub struct InsertGenericInferenceForFalArgs<'e, 'c, E>
where E: 'e + Executor<'c, Database = MySql>
{
pub uuid_idempotency_token: &'e str,
/// The external primary key identifier for the job.
pub maybe_external_third_party_id: &'a str,
pub maybe_external_third_party_id: &'e str,
pub fal_category: FalCategory,
pub maybe_inference_args: Option<GenericInferenceArgs>,
pub maybe_prompt_token: Option<&'e PromptToken>,
pub maybe_creator_user_token: Option<&'a UserToken>,
pub maybe_avt_token: Option<&'a AnonymousVisitorTrackingToken>,
pub creator_ip_address: &'a str,
pub maybe_creator_user_token: Option<&'e UserToken>,
pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>,
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
pub mysql_pool: &'a MySqlPool,
pub mysql_executor: E,
// TODO: Not sure if this works to tell the compiler we need the lifetime annotation.
// See: https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-lifetime-parameters
pub phantom: PhantomData<&'c E>,
}
pub async fn insert_generic_inference_job_for_fal_queue(args: InsertGenericInferenceForFalArgs<'_>)
pub async fn insert_generic_inference_job_for_fal_queue<'e, 'c : 'e, E>(args: InsertGenericInferenceForFalArgs<'e, 'c, E>)
-> Result<InferenceJobToken, DatabaseQueryError>
where E: 'e + Executor<'c, Database = MySql>
{
let job_token = InferenceJobToken::generate();
@@ -105,6 +117,8 @@ SET
maybe_download_url = NULL,
maybe_cover_image_media_file_token = NULL,
maybe_prompt_token = ?,
maybe_raw_inference_text = NULL,
@@ -134,6 +148,8 @@ SET
product_category.to_str(),
inference_category.to_str(),
args.maybe_prompt_token.map(|t| t.to_string()),
serialized_args_payload,
@@ -145,7 +161,7 @@ SET
STATUS.to_str(),
);
let query_result = query.execute(args.mysql_pool)
let query_result = query.execute(args.mysql_executor)
.await;
let record_id = match query_result {
@@ -2,6 +2,7 @@ use std::collections::BTreeSet;
use anyhow::anyhow;
use chrono::Utc;
use log::debug;
use sqlx::MySqlPool;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
@@ -151,10 +152,8 @@ pub async fn list_available_generic_inference_jobs(
.map(|token_type| InferenceInputSourceTokenType::from_str(token_type)
.map_err(|err| anyhow!("error: {:?}", err)))
.transpose()?,
maybe_inference_args: record.maybe_inference_args
.as_deref()
.map(|args| GenericInferenceArgs::from_json(args))
.transpose()?,
maybe_inference_args: GenericInferenceArgs::from_json_optional(
record.maybe_inference_args.as_deref())?,
maybe_raw_inference_text: record.maybe_raw_inference_text,
status: JobStatusPlus::from_str(&record.status)
.map_err(|err| anyhow!("JobStatus failure to parse: {:?}", err))?,
@@ -5,3 +5,4 @@ pub mod mark_generic_inference_job_completely_failed;
pub mod mark_generic_inference_job_failure;
pub mod mark_generic_inference_job_pending_and_grab_lock;
pub mod mark_generic_inference_job_successfully_done;
pub mod reap_stale_fakeyou_jobs;
@@ -0,0 +1,52 @@
use anyhow::anyhow;
use log::info;
use sqlx;
use sqlx::pool::PoolConnection;
use sqlx::{MySql, MySqlPool};
use errors::AnyhowResult;
/// Kill FakeYou jobs that are "stale".
///
/// - This leaves Fal jobs alone, as they do not use the job runner.
/// - This kills jobs that are over a threshold (e.g. an hour) overdue.
pub async fn reap_stale_fakeyou_jobs(mysql_pool: &MySqlPool) -> AnyhowResult<()>
{
let mut connection = mysql_pool.acquire().await?;
reap_stale_fakeyou_jobs_using_connection(&mut connection).await
}
pub async fn reap_stale_fakeyou_jobs_using_connection(
mysql_connection: &mut PoolConnection<MySql>,
) -> AnyhowResult<()> {
let query_result = sqlx::query!(
r#"
UPDATE generic_inference_jobs
SET
status = 'cancelled_by_system',
retry_at = NULL,
internal_debugging_failure_reason = 'reap_stale_fakeyou_jobs'
WHERE
job_type != 'fal_queue'
AND status IN (
'pending',
'started',
'attempt_failed'
)
AND created_at > NOW() - INTERVAL 30 MINUTE
LIMIT 5000
"#
)
.execute(&mut **mysql_connection)
.await;
match query_result {
Ok(result) => {
info!("Reaped stale FakeYou jobs: {} rows affected.", result.rows_affected());
Ok(())
},
Err(err) => {
Err(anyhow!("error with reaping stale jobs: {:?}", err))
}
}
}
@@ -1,19 +1,18 @@
use crate::queries::media_files::create::generic_insert::insert_media_file_generic::{insert_media_file_generic, InsertArgs};
use anyhow::anyhow;
use crate::queries::media_files::create::insert_builder::media_file_insert_builder_error::MediaFileInsertBuilderError;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::generic_synthetic_ids::id_category::IdCategory;
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_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use sqlx::MySqlPool;
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
use crate::queries::media_files::create::insert_builder::media_file_insert_builder_error::MediaFileInsertBuilderError;
/// Builder for inserting a media files into the database.
/// Resembles an ORM, but can still use statically type checked queries (which is probably
@@ -53,7 +52,9 @@ pub struct MediaFileInsertBuilder {
checksum_sha2: Option<String>, // NB: Non-nullable field
// Media info for certain product areas
pub maybe_engine_category: Option<MediaFileEngineCategory>, // TODO: Deprecate
maybe_engine_category: Option<MediaFileEngineCategory>, // TODO: Deprecate
maybe_prompt_token: Option<PromptToken>,
// // User text information
// maybe_title: Option<&'a str>,
@@ -100,6 +101,7 @@ impl MediaFileInsertBuilder {
checksum_sha2: None,
maybe_engine_category: None,
public_bucket_directory_hash: None,
maybe_prompt_token: None,
}
}
@@ -165,6 +167,11 @@ impl MediaFileInsertBuilder {
self
}
pub fn maybe_prompt_token(mut self, maybe_prompt_token: Option<&PromptToken>) -> Self {
self.maybe_prompt_token = maybe_prompt_token.map(|token| token.clone());
self
}
// TODO: Method for maybe_origin_model_type
// TODO: Method for maybe_origin_model_token
// TODO: Method for maybe_origin_filename
@@ -273,7 +280,7 @@ impl MediaFileInsertBuilder {
maybe_title: None, // TODO
maybe_text_transcript: None, // TODO
maybe_scene_source_media_file_token: None, // TODO
maybe_prompt_token: None, // TODO
maybe_prompt_token: self.maybe_prompt_token.as_ref(),
maybe_batch_token: None, // TODO
public_bucket_directory_hash: bucket_path.get_object_hash(),
maybe_public_bucket_prefix: bucket_path.get_optional_prefix(),
@@ -1,7 +1,7 @@
use chrono::{DateTime, Utc};
use sqlx::mysql::MySqlRow;
use sqlx::{Acquire, FromRow, MySql, MySqlPool, QueryBuilder, Row};
use sqlx::pool::PoolConnection;
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;
@@ -84,6 +84,18 @@ pub async fn batch_get_media_files_by_tokens(
).await
}
pub async fn batch_get_media_files_by_tokens_with_connection(
mysql_connection: &mut PoolConnection<MySql>,
media_file_tokens: &[MediaFileToken],
can_see_deleted: bool
) -> AnyhowResult<Vec<MediaFilesByTokensRecord>> {
batch_get_media_files_by_tokens_with_transactor(
Transactor::Connection{ connection: mysql_connection },
media_file_tokens,
can_see_deleted
).await
}
pub async fn batch_get_media_files_by_tokens_with_transactor(
transactor: Transactor<'_, '_>,
media_file_tokens: &[MediaFileToken],
@@ -5,8 +5,6 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use sqlx::MySqlPool;
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;
@@ -16,6 +14,8 @@ use enums::by_table::model_weights::weights_category::WeightsCategory;
use enums::by_table::model_weights::weights_types::WeightsType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use sqlx::pool::PoolConnection;
use sqlx::{MySql, MySqlPool};
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
@@ -215,6 +215,18 @@ pub async fn get_media_file(
).await
}
pub async fn get_media_file_with_connection(
media_file_token: &MediaFileToken,
can_see_deleted: bool,
mysql_connection: &mut PoolConnection<MySql>,
) -> AnyhowResult<Option<MediaFile>> {
get_media_file_with_transactor(
media_file_token,
can_see_deleted,
Transactor::for_connection(mysql_connection)
).await
}
pub async fn get_media_file_with_transactor(
media_file_token: &MediaFileToken,
can_see_deleted: bool,
@@ -21,6 +21,7 @@ pub mod media_uploads;
pub mod model_categories;
pub mod model_weight_usage_counts;
pub mod model_weights;
pub mod prompt_context_items;
pub mod prompts;
pub mod public_event_feed;
pub mod stats;
@@ -0,0 +1,58 @@
use anyhow::anyhow;
use log::error;
use sqlx::{MySql, QueryBuilder, Transaction};
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use errors::AnyhowResult;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
pub struct PromptContextItem {
pub media_token: MediaFileToken,
pub context_semantic_type: PromptContextSemanticType,
}
pub struct InsertBatchArgs<'a, 'b> {
pub prompt_token: PromptToken,
pub items: Vec<PromptContextItem>,
pub transaction: &'a mut Transaction<'b, MySql>,
}
/// Insert a list of entities into a "batch" together for grouping; returns the new batch token identifier to return
/// to the HTTP caller.
///
/// NB: Calling code is responsible for rolling back the transaction if this fails.
pub async fn insert_batch_prompt_context_items<'a, 'b>(args: InsertBatchArgs<'a, 'b>) -> AnyhowResult<()> {
let mut query_builder = QueryBuilder::new(r#"
INSERT INTO prompt_context_items (prompt_token, media_token, context_semantic_type) VALUES
"#);
for (i, item) in args.items.iter().enumerate() {
query_builder.push("(");
query_builder.push_bind(&args.prompt_token);
query_builder.push(",");
query_builder.push_bind(item.media_token.to_string());
query_builder.push(",");
query_builder.push_bind(item.context_semantic_type.to_string());
query_builder.push(")");
if i < args.items.len() - 1 {
query_builder.push(",");
}
}
let query = query_builder.build();
let query_result = query.execute(&mut **args.transaction).await;
match query_result {
Ok(_) => Ok(()),
Err(err) => {
error!("Error with prompt context item insert query: {:?}", &err);
Err(anyhow!("prompt context item insert error: {:?}", &err))
},
}
}
@@ -0,0 +1,75 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use errors::AnyhowResult;
use log::{info, warn};
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
pub struct PromptContextItem {
pub media_token: MediaFileToken,
pub context_semantic_type: PromptContextSemanticType,
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
}
struct RawPromptContextItem {
media_token: MediaFileToken,
context_semantic_type: PromptContextSemanticType,
public_bucket_directory_hash: String,
maybe_public_bucket_prefix: Option<String>,
maybe_public_bucket_extension: Option<String>,
}
pub async fn list_prompt_context_items(
prompt_token: &PromptToken,
mysql_connection: &mut PoolConnection<MySql>,
) -> AnyhowResult<Vec<PromptContextItem>> {
let result = sqlx::query_as!(
RawPromptContextItem,
r#"
SELECT
pci.media_token as `media_token: tokens::tokens::media_files::MediaFileToken`,
pci.context_semantic_type as `context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType`,
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension
FROM prompt_context_items pci
JOIN media_files m
ON
pci.media_token = m.token
WHERE
pci.prompt_token = ?
ORDER BY pci.id ASC
"#,
prompt_token.as_str()
)
.fetch_all(&mut **mysql_connection)
.await;
match result {
Ok(items) => {
Ok(items.into_iter().map(|item| {
PromptContextItem {
media_token: item.media_token,
context_semantic_type: item.context_semantic_type,
public_bucket_directory_hash: item.public_bucket_directory_hash,
maybe_public_bucket_prefix: item.maybe_public_bucket_prefix,
maybe_public_bucket_extension: item.maybe_public_bucket_extension,
}
}).collect::<Vec<PromptContextItem>>())
}
Err(err) => match err {
sqlx::Error::RowNotFound => Ok(Vec::new()),
_ => {
warn!("Error querying prompt context items: {:?}", err);
Err(anyhow!("error querying prompt context items"))
}
}
}
}
@@ -0,0 +1,2 @@
pub mod insert_batch_prompt_context_items;
pub mod list_prompt_context_items;
@@ -5,10 +5,12 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use sqlx::MySqlPool;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use errors::AnyhowResult;
use sqlx::pool::PoolConnection;
use sqlx::{MySql, MySqlPool};
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
@@ -20,6 +22,9 @@ pub struct Prompt {
pub prompt_type: PromptType,
pub maybe_model_type: Option<ModelType>,
pub maybe_generation_provider: Option<GenerationProvider>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_positive_prompt: Option<String>,
@@ -39,6 +44,9 @@ pub struct PromptRaw {
pub prompt_type: PromptType,
pub maybe_model_type: Option<ModelType>,
pub maybe_generation_provider: Option<GenerationProvider>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_positive_prompt: Option<String>,
@@ -52,12 +60,21 @@ pub struct PromptRaw {
pub created_at: DateTime<Utc>,
}
pub async fn get_prompt(
prompt_token: &PromptToken,
mysql_pool: &MySqlPool
) -> AnyhowResult<Option<Prompt>> {
let mut connection = mysql_pool.acquire().await?;
get_prompt_from_connection(prompt_token, &mut connection).await
}
let record = select_record(prompt_token, mysql_pool).await;
pub async fn get_prompt_from_connection(
prompt_token: &PromptToken,
mysql_connection: &mut PoolConnection<MySql>
) -> AnyhowResult<Option<Prompt>> {
let record = select_record(prompt_token, mysql_connection).await;
let record = match record {
Ok(record) => record,
@@ -72,6 +89,8 @@ pub async fn get_prompt(
Ok(Some(Prompt {
token: record.token,
prompt_type: record.prompt_type,
maybe_model_type: record.maybe_model_type,
maybe_generation_provider: record.maybe_generation_provider,
maybe_creator_user_token: record.maybe_creator_user_token,
maybe_positive_prompt: record.maybe_positive_prompt,
maybe_negative_prompt: record.maybe_negative_prompt,
@@ -86,7 +105,7 @@ pub async fn get_prompt(
async fn select_record(
prompt_token: &PromptToken,
mysql_pool: &MySqlPool
mysql_connection: &mut PoolConnection<MySql>
) -> Result<PromptRaw, sqlx::Error> {
sqlx::query_as!(
PromptRaw,
@@ -96,6 +115,9 @@ SELECT
p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,
p.maybe_model_type as `maybe_model_type: enums::common::model_type::ModelType`,
p.maybe_generation_provider as `maybe_generation_provider: enums::common::generation_provider::GenerationProvider`,
p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
p.maybe_positive_prompt,
@@ -111,6 +133,6 @@ WHERE
"#,
prompt_token
)
.fetch_one(mysql_pool)
.fetch_one(&mut **mysql_connection)
.await
}
@@ -6,6 +6,8 @@ use sqlx;
use sqlx::{Executor, MySql};
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use errors::AnyhowResult;
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
@@ -21,6 +23,10 @@ pub struct InsertPromptArgs<'e, 'c, E>
pub prompt_type: PromptType,
pub maybe_creator_user_token: Option<&'e UserToken>,
pub maybe_model_type: Option<ModelType>,
pub maybe_generation_provider: Option<GenerationProvider>,
pub maybe_positive_prompt: Option<&'e str>,
@@ -65,9 +71,13 @@ SET
prompt_type = ?,
maybe_creator_user_token = ?,
maybe_model_type = ?,
maybe_generation_provider = ?,
maybe_positive_prompt = ?,
maybe_negative_prompt = ?,
maybe_other_args = ?,
creator_ip_address = ?
@@ -75,6 +85,8 @@ SET
prompt_token.as_str(),
args.prompt_type.to_str(),
args.maybe_creator_user_token.map(|t| t.as_str()),
args.maybe_model_type.map(|m| m.to_str()),
args.maybe_generation_provider.map(|g| g.to_str()),
args.maybe_positive_prompt,
args.maybe_negative_prompt,
maybe_other_args,
@@ -0,0 +1,58 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GPT_IMAGE_1_TEXT_TO_IMAGE_PATH: &str = "/v1/generate/image/gpt_image_1_text_to_image";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateGptImage1TextToImageRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Size of the image to generate. Default is Square.
pub image_size: Option<GenerateGptImage1TextToImageImageSize>,
/// Number of images to generate. Default is one.
pub num_images: Option<GenerateGptImage1TextToImageNumImages>,
/// Quality of the image to generate. Default is High.
pub image_quality: Option<GenerateGptImage1TextToImageImageQuality>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateGptImage1TextToImageImageSize {
Square, // 1:1, Default
Horizontal,
Vertical,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateGptImage1TextToImageNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateGptImage1TextToImageImageQuality {
Auto,
Low,
Medium,
High, // Default
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateGptImage1TextToImageResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -3,4 +3,5 @@ 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 remove_image_background;
@@ -259,7 +259,8 @@ mod tests {
#[test]
fn serialized_length_ok_for_database() {
const MAX_LENGTH : usize = 32;
// NB: The media_files table has allocated width for VARCHAR(32), but let's slim it down to 24.
const MAX_LENGTH : usize = 24;
for variant in MediaFileOriginModelType::all_variants() {
let serialized = variant.to_str();
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
@@ -13,6 +13,7 @@ pub mod media_files;
pub mod media_uploads;
pub mod model_categories;
pub mod model_weights;
pub mod prompt_context_items;
pub mod prompts;
pub mod tag_uses;
pub mod trending_model_analytics;
@@ -0,0 +1 @@
pub mod prompt_context_semantic_type;
@@ -0,0 +1,163 @@
use std::collections::BTreeSet;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
use utoipa::ToSchema;
/// Used in the `prompt_context_items` table in a `VARCHAR(16)` field.
///
/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum PromptContextSemanticType {
/// Image-to-Video starting frame
VidStartFrame,
/// Image-to-Video ending frame
VidEndFrame,
/// Standard image reference without a semantic type (e.g. Sora/ChatGPT 4o/gpt-image-1)
Imgref,
ImgrefCharacter,
ImgrefStyle,
ImgrefBg,
}
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
impl_enum_display_and_debug_using_to_str!(PromptContextSemanticType);
impl_mysql_enum_coders!(PromptContextSemanticType);
impl_mysql_from_row!(PromptContextSemanticType);
/// NB: Legacy API for older code.
impl PromptContextSemanticType {
pub fn to_str(&self) -> &'static str {
match self {
Self::VidStartFrame => "vid_start_frame",
Self::VidEndFrame => "vid_end_frame",
Self::Imgref => "imgref",
Self::ImgrefCharacter => "imgref_character",
Self::ImgrefStyle => "imgref_style",
Self::ImgrefBg => "imgref_bg",
}
}
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"vid_start_frame" => Ok(Self::VidStartFrame),
"vid_end_frame" => Ok(Self::VidEndFrame),
"imgref" => Ok(Self::Imgref),
"imgref_character" => Ok(Self::ImgrefCharacter),
"imgref_style" => Ok(Self::ImgrefStyle),
"imgref_bg" => Ok(Self::ImgrefBg),
_ => Err(format!("invalid value: {:?}", value)),
}
}
pub fn all_variants() -> BTreeSet<Self> {
// NB: BTreeSet is sorted
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::VidStartFrame,
Self::VidEndFrame,
Self::Imgref,
Self::ImgrefCharacter,
Self::ImgrefStyle,
Self::ImgrefBg,
])
}
}
#[cfg(test)]
mod tests {
use crate::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use crate::test_helpers::assert_serialization;
mod serde {
use super::*;
#[test]
fn test_serialization() {
assert_serialization(PromptContextSemanticType::VidStartFrame, "vid_start_frame");
assert_serialization(PromptContextSemanticType::VidEndFrame, "vid_end_frame");
assert_serialization(PromptContextSemanticType::Imgref, "imgref");
assert_serialization(PromptContextSemanticType::ImgrefCharacter, "imgref_character");
assert_serialization(PromptContextSemanticType::ImgrefStyle, "imgref_style");
assert_serialization(PromptContextSemanticType::ImgrefBg, "imgref_bg");
}
}
mod impl_methods {
use super::*;
#[test]
fn to_str() {
assert_eq!(PromptContextSemanticType::VidStartFrame.to_str(), "vid_start_frame");
assert_eq!(PromptContextSemanticType::VidEndFrame.to_str(), "vid_end_frame");
assert_eq!(PromptContextSemanticType::Imgref.to_str(), "imgref");
assert_eq!(PromptContextSemanticType::ImgrefCharacter.to_str(), "imgref_character");
assert_eq!(PromptContextSemanticType::ImgrefStyle.to_str(), "imgref_style");
assert_eq!(PromptContextSemanticType::ImgrefBg.to_str(), "imgref_bg");
}
#[test]
fn from_str() {
assert_eq!(PromptContextSemanticType::from_str("vid_start_frame").unwrap(), PromptContextSemanticType::VidStartFrame);
assert_eq!(PromptContextSemanticType::from_str("vid_end_frame").unwrap(), PromptContextSemanticType::VidEndFrame);
assert_eq!(PromptContextSemanticType::from_str("imgref").unwrap(), PromptContextSemanticType::Imgref);
assert_eq!(PromptContextSemanticType::from_str("imgref_character").unwrap(), PromptContextSemanticType::ImgrefCharacter);
assert_eq!(PromptContextSemanticType::from_str("imgref_style").unwrap(), PromptContextSemanticType::ImgrefStyle);
assert_eq!(PromptContextSemanticType::from_str("imgref_bg").unwrap(), PromptContextSemanticType::ImgrefBg);
assert!(PromptContextSemanticType::from_str("foo").is_err());
}
}
mod manual_variant_checks {
use super::*;
#[test]
fn all_variants() {
let mut variants = PromptContextSemanticType::all_variants();
assert_eq!(variants.len(), 6);
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::VidStartFrame));
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::VidEndFrame));
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::Imgref));
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::ImgrefCharacter));
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::ImgrefStyle));
assert_eq!(variants.pop_first(), Some(PromptContextSemanticType::ImgrefBg));
assert_eq!(variants.pop_first(), None);
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(PromptContextSemanticType::all_variants().len(), PromptContextSemanticType::iter().len());
}
#[test]
fn round_trip() {
for variant in PromptContextSemanticType::all_variants() {
assert_eq!(variant, PromptContextSemanticType::from_str(variant.to_str()).unwrap());
assert_eq!(variant, PromptContextSemanticType::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, PromptContextSemanticType::from_str(&format!("{:?}", variant)).unwrap());
}
}
#[test]
fn serialized_length_ok_for_database() {
const MAX_LENGTH : usize = 16;
for variant in PromptContextSemanticType::all_variants() {
let serialized = variant.to_str();
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
}
}
}
}
@@ -13,10 +13,15 @@ use utoipa::ToSchema;
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum PromptType {
/// Artcraft (App)
ArtcraftApp,
/// Stable diffusion
#[deprecated]
StableDiffusion,
/// Comfy UI
#[deprecated]
ComfyUi,
}
@@ -29,6 +34,7 @@ impl_mysql_from_row!(PromptType);
impl PromptType {
pub fn to_str(&self) -> &'static str {
match self {
Self::ArtcraftApp => "artcraft_app",
Self::StableDiffusion => "stable_diffusion",
Self::ComfyUi => "comfy_ui",
}
@@ -36,6 +42,7 @@ impl PromptType {
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"artcraft_app" => Ok(Self::ArtcraftApp),
"stable_diffusion" => Ok(Self::StableDiffusion),
"comfy_ui" => Ok(Self::ComfyUi),
_ => Err(format!("invalid value: {:?}", value)),
@@ -46,6 +53,7 @@ impl PromptType {
// NB: BTreeSet is sorted
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::ArtcraftApp,
Self::StableDiffusion,
Self::ComfyUi,
])
@@ -62,6 +70,7 @@ mod tests {
#[test]
fn test_serialization() {
assert_serialization(PromptType::ArtcraftApp, "artcraft_app");
assert_serialization(PromptType::StableDiffusion, "stable_diffusion");
assert_serialization(PromptType::ComfyUi, "comfy_ui");
}
@@ -72,12 +81,14 @@ mod tests {
#[test]
fn to_str() {
assert_eq!(PromptType::ArtcraftApp.to_str(), "artcraft_app");
assert_eq!(PromptType::StableDiffusion.to_str(), "stable_diffusion");
assert_eq!(PromptType::ComfyUi.to_str(), "comfy_ui");
}
#[test]
fn from_str() {
assert_eq!(PromptType::from_str("artcraft_app").unwrap(), PromptType::ArtcraftApp);
assert_eq!(PromptType::from_str("stable_diffusion").unwrap(), PromptType::StableDiffusion);
assert_eq!(PromptType::from_str("comfy_ui").unwrap(), PromptType::ComfyUi);
assert!(PromptType::from_str("foo").is_err());
@@ -90,7 +101,8 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = PromptType::all_variants();
assert_eq!(variants.len(), 2);
assert_eq!(variants.len(), 3);
assert_eq!(variants.pop_first(), Some(PromptType::ArtcraftApp));
assert_eq!(variants.pop_first(), Some(PromptType::StableDiffusion));
assert_eq!(variants.pop_first(), Some(PromptType::ComfyUi));
assert_eq!(variants.pop_first(), None);
@@ -6,6 +6,8 @@ use strum::EnumCount;
use strum::EnumIter;
use utoipa::ToSchema;
/// NB: This will be used by a variety of tables (MySQL and sqlite)!
/// Keep the max length to 16 characters.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
@@ -108,5 +110,15 @@ mod tests {
assert_eq!(variant, GenerationProvider::from_str(&format!("{:?}", variant)).unwrap());
}
}
#[test]
fn serialized_length_ok_for_database() {
const MAX_LENGTH : usize = 16;
for variant in GenerationProvider::all_variants() {
let serialized = variant.to_str();
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
}
}
}
}
@@ -1,6 +1,7 @@
pub mod generation_provider;
pub mod job_status;
pub mod job_status_plus;
pub mod model_type;
pub mod sqlite;
pub mod view_as;
pub mod visibility;
@@ -0,0 +1,253 @@
use std::collections::BTreeSet;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
use utoipa::ToSchema;
/// NB: This will be used by a variety of tables (MySQL and sqlite)!
/// Keep the max length to 24 characters.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum ModelType {
// Image models
#[serde(rename = "flux_1_dev")]
Flux1Dev,
#[serde(rename = "flux_1_schnell")]
Flux1Schnell,
#[serde(rename = "flux_pro_1p1")]
FluxPro11,
#[serde(rename = "flux_pro_1p1_ultra")]
FluxPro11Ultra,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "recraft_3")]
Recraft3,
// Video models
#[serde(rename = "kling_1p6_pro")]
Kling16Pro,
#[serde(rename = "kling_2p1_pro")]
Kling21Pro,
#[serde(rename = "kling_2p1_master")]
Kling21Master,
#[serde(rename = "seedance_1p0_lite")]
Seedance10Lite,
#[serde(rename = "veo_2")]
Veo2,
// 3D Object generation models
#[serde(rename = "hunyuan_3d_2p0")]
Hunyuan3d2_0,
#[serde(rename = "hunyuan_3d_2p1")]
Hunyuan3d2_1,
}
impl_enum_display_and_debug_using_to_str!(ModelType);
impl_mysql_enum_coders!(ModelType);
impl_mysql_from_row!(ModelType);
// NB: We can derive `sqlx::Type` instead of using `impl_mysql_enum_coders`
impl ModelType {
pub fn to_str(&self) -> &'static str {
match self {
// Image models
Self::Flux1Dev => "flux_1_dev",
Self::Flux1Schnell => "flux_1_schnell",
Self::FluxPro11 => "flux_pro_1p1",
Self::FluxPro11Ultra => "flux_pro_1p1_ultra",
Self::GptImage1 => "gpt_image_1",
Self::Recraft3 => "recraft_3",
// Video models
Self::Kling16Pro => "kling_1p6_pro",
Self::Kling21Pro => "kling_2p1_pro",
Self::Kling21Master => "kling_2p1_master",
Self::Seedance10Lite => "seedance_1p0_lite",
Self::Veo2 => "veo_2",
// 3D Object generation models
Self::Hunyuan3d2_0 => "hunyuan_3d_2p0",
Self::Hunyuan3d2_1 => "hunyuan_3d_2p1",
}
}
pub fn from_str(job_status: &str) -> Result<Self, String> {
match job_status {
// Image models
"flux_1_dev" => Ok(Self::Flux1Dev),
"flux_1_schnell" => Ok(Self::Flux1Schnell),
"flux_pro_1p1" => Ok(Self::FluxPro11),
"flux_pro_1p1_ultra" => Ok(Self::FluxPro11Ultra),
"gpt_image_1" => Ok(Self::GptImage1),
"recraft_3" => Ok(Self::Recraft3),
// Video models
"kling_1p6_pro" => Ok(Self::Kling16Pro),
"kling_2p1_pro" => Ok(Self::Kling21Pro),
"kling_2p1_master" => Ok(Self::Kling21Master),
"seedance_1p0_lite" => Ok(Self::Seedance10Lite),
"veo_2" => Ok(Self::Veo2),
// 3D Object generation models
"hunyuan_3d_2p0" => Ok(Self::Hunyuan3d2_0),
"hunyuan_3d_2p1" => Ok(Self::Hunyuan3d2_1),
_ => Err(format!("invalid task_state: {:?}", job_status)),
}
}
pub fn all_variants() -> BTreeSet<Self> {
// NB: BTreeSet is sorted
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
// Image models
Self::Flux1Dev,
Self::Flux1Schnell,
Self::FluxPro11,
Self::FluxPro11Ultra,
Self::GptImage1,
Self::Recraft3,
// Video models
Self::Kling16Pro,
Self::Kling21Pro,
Self::Kling21Master,
Self::Seedance10Lite,
Self::Veo2,
// 3D Object generation models
Self::Hunyuan3d2_0,
Self::Hunyuan3d2_1,
])
}
}
#[cfg(test)]
mod tests {
use crate::common::model_type::ModelType;
use crate::test_helpers::assert_serialization;
mod explicit_checks {
use super::*;
#[test]
fn test_serialization() {
// Image models
assert_serialization(ModelType::Flux1Dev, "flux_1_dev");
assert_serialization(ModelType::Flux1Schnell, "flux_1_schnell");
assert_serialization(ModelType::FluxPro11, "flux_pro_1p1");
assert_serialization(ModelType::FluxPro11Ultra, "flux_pro_1p1_ultra");
assert_serialization(ModelType::GptImage1, "gpt_image_1");
assert_serialization(ModelType::Recraft3, "recraft_3");
// Video models
assert_serialization(ModelType::Kling16Pro, "kling_1p6_pro");
assert_serialization(ModelType::Kling21Pro, "kling_2p1_pro");
assert_serialization(ModelType::Kling21Master, "kling_2p1_master");
assert_serialization(ModelType::Seedance10Lite, "seedance_1p0_lite");
assert_serialization(ModelType::Veo2, "veo_2");
// 3D Object generation models
assert_serialization(ModelType::Hunyuan3d2_0, "hunyuan_3d_2p0");
assert_serialization(ModelType::Hunyuan3d2_1, "hunyuan_3d_2p1");
}
#[test]
fn to_str() {
// Image models
assert_eq!(ModelType::Flux1Dev.to_str(), "flux_1_dev");
assert_eq!(ModelType::Flux1Schnell.to_str(), "flux_1_schnell");
assert_eq!(ModelType::FluxPro11.to_str(), "flux_pro_1p1");
assert_eq!(ModelType::FluxPro11Ultra.to_str(), "flux_pro_1p1_ultra");
assert_eq!(ModelType::GptImage1.to_str(), "gpt_image_1");
assert_eq!(ModelType::Recraft3.to_str(), "recraft_3");
// Video models
assert_eq!(ModelType::Kling16Pro.to_str(), "kling_1p6_pro");
assert_eq!(ModelType::Kling21Pro.to_str(), "kling_2p1_pro");
assert_eq!(ModelType::Kling21Master.to_str(), "kling_2p1_master");
assert_eq!(ModelType::Seedance10Lite.to_str(), "seedance_1p0_lite");
assert_eq!(ModelType::Veo2.to_str(), "veo_2");
// 3D Object generation models
assert_eq!(ModelType::Hunyuan3d2_0.to_str(), "hunyuan_3d_2p0");
assert_eq!(ModelType::Hunyuan3d2_1.to_str(), "hunyuan_3d_2p1");
}
#[test]
fn from_str() {
// Image models
assert_eq!(ModelType::from_str("flux_1_dev").unwrap(), ModelType::Flux1Dev);
assert_eq!(ModelType::from_str("flux_1_schnell").unwrap(), ModelType::Flux1Schnell);
assert_eq!(ModelType::from_str("flux_pro_1p1").unwrap(), ModelType::FluxPro11);
assert_eq!(ModelType::from_str("flux_pro_1p1_ultra").unwrap(), ModelType::FluxPro11Ultra);
assert_eq!(ModelType::from_str("gpt_image_1").unwrap(), ModelType::GptImage1);
assert_eq!(ModelType::from_str("recraft_3").unwrap(), ModelType::Recraft3);
// Video models
assert_eq!(ModelType::from_str("kling_1p6_pro").unwrap(), ModelType::Kling16Pro);
assert_eq!(ModelType::from_str("kling_2p1_pro").unwrap(), ModelType::Kling21Pro);
assert_eq!(ModelType::from_str("kling_2p1_master").unwrap(), ModelType::Kling21Master);
assert_eq!(ModelType::from_str("seedance_1p0_lite").unwrap(), ModelType::Seedance10Lite);
assert_eq!(ModelType::from_str("veo_2").unwrap(), ModelType::Veo2);
// 3D Object generation models
assert_eq!(ModelType::from_str("hunyuan_3d_2p0").unwrap(), ModelType::Hunyuan3d2_0);
assert_eq!(ModelType::from_str("hunyuan_3d_2p1").unwrap(), ModelType::Hunyuan3d2_1);
}
#[test]
fn all_variants() {
let mut variants = ModelType::all_variants();
assert_eq!(variants.len(), 13);
// Image models
assert_eq!(variants.pop_first(), Some(ModelType::Flux1Dev));
assert_eq!(variants.pop_first(), Some(ModelType::Flux1Schnell));
assert_eq!(variants.pop_first(), Some(ModelType::FluxPro11));
assert_eq!(variants.pop_first(), Some(ModelType::FluxPro11Ultra));
assert_eq!(variants.pop_first(), Some(ModelType::GptImage1));
assert_eq!(variants.pop_first(), Some(ModelType::Recraft3));
// Video models
assert_eq!(variants.pop_first(), Some(ModelType::Kling16Pro));
assert_eq!(variants.pop_first(), Some(ModelType::Kling21Pro));
assert_eq!(variants.pop_first(), Some(ModelType::Kling21Master));
assert_eq!(variants.pop_first(), Some(ModelType::Seedance10Lite));
assert_eq!(variants.pop_first(), Some(ModelType::Veo2));
// 3D Object generation models
assert_eq!(variants.pop_first(), Some(ModelType::Hunyuan3d2_0));
assert_eq!(variants.pop_first(), Some(ModelType::Hunyuan3d2_1));
assert_eq!(variants.pop_first(), None);
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(ModelType::all_variants().len(), ModelType::iter().len());
}
#[test]
fn round_trip() {
for variant in ModelType::all_variants() {
// Test to_str(), from_str(), Display, and Debug.
assert_eq!(variant, ModelType::from_str(variant.to_str()).unwrap());
assert_eq!(variant, ModelType::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, ModelType::from_str(&format!("{:?}", variant)).unwrap());
}
}
#[test]
fn serialized_length_ok_for_database() {
const MAX_LENGTH : usize = 24;
for variant in ModelType::all_variants() {
let serialized = variant.to_str();
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
}
}
}
}
@@ -40,6 +40,8 @@ pub fn run_http_server(args: CreateServerArgs) -> AnyhowResult<Server>
// NB: We shouldn't be logging much as the /_status endpoint is all we aim to expose.
let log_format = "[%{HOSTNAME}e] IP=[%{X-Forwarded-For}i] \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T";
info!("HTTP server will bind to: {}", bind_address);
let handle = HttpServer::new(move || {
// NB: app_data being clone()'d below should all be safe (dependencies included)
App::new()
@@ -15,6 +15,8 @@ use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginM
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use filesys::path_to_string::path_to_string;
use mysql_queries::payloads::media_file_extra_info::inner_payloads::stable_diffusion_extra_info::StableDiffusionExtraInfo;
use mysql_queries::payloads::media_file_extra_info::media_file_extra_info::MediaFileExtraInfo;
@@ -328,6 +330,8 @@ pub async fn process_job_inference(
maybe_apriori_prompt_token: Some(&prompt_token),
prompt_type: PromptType::StableDiffusion,
maybe_creator_user_token: job.maybe_creator_user_token_typed.as_ref(),
maybe_model_type: None,
maybe_generation_provider: None,
maybe_positive_prompt: Some(&positive_prompt),
maybe_negative_prompt: maybe_negative_prompt.as_deref(),
maybe_other_args: None, // TODO(bt,2024-02-22): Support other arguments
@@ -8,6 +8,8 @@ use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path:
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use filesys::file_size::file_size;
use filesys::path_to_string::path_to_string;
use hashing::sha256::sha256_hash_file::sha256_hash_file;
@@ -290,6 +292,8 @@ pub async fn validate_and_save_results(args: SaveResultsArgs<'_>) -> Result<Medi
maybe_apriori_prompt_token: Some(&prompt_token),
prompt_type: PromptType::ComfyUi,
maybe_creator_user_token: args.job.maybe_creator_user_token_typed.as_ref(),
maybe_model_type: None,
maybe_generation_provider: None,
maybe_positive_prompt: args.comfy_args.positive_prompt.as_deref(),
maybe_negative_prompt: args.comfy_args.negative_prompt.as_deref(),
maybe_other_args: maybe_other_args.as_ref(),
@@ -53,6 +53,7 @@ use crate::state::job_dependencies::{BucketDependencies, ClientDependencies, Dat
use crate::state::job_specific_dependencies::JobSpecificDependencies;
use crate::state::scoped_job_type_execution::ScopedJobTypeExecution;
use crate::state::scoped_model_type_execution::ScopedModelTypeExecution;
use crate::threads::reap_jobs_thread::reap_jobs_thread;
use crate::util::filesystem::scoped_temp_dir_creator::ScopedTempDirCreator;
use crate::util::instrumentation::JobInstruments;
use crate::util::instrumentation::{init_otel_metrics_pipeline, JobInstrumentLabels};
@@ -61,6 +62,7 @@ use crate::util::model_weights_cache::model_weights_cache_directory::ModelWeight
pub mod http_server;
pub mod job;
pub mod state;
pub mod threads;
pub mod util;
// Buckets (shared config)
@@ -163,6 +165,8 @@ async fn main() -> AnyhowResult<()> {
.connect(&db_connection_string)
.await?;
let mysql_pool_2 = mysql_pool.clone();
info!("Connected to MySQL.");
let common_env = CommonEnv::read_from_env()?;
@@ -345,6 +349,14 @@ async fn main() -> AnyhowResult<()> {
set_up_directories(&job_dependencies)?;
std::thread::spawn(move || {
let actix_runtime = actix_web::rt::System::new();
actix_runtime.block_on(reap_jobs_thread(mysql_pool_2));
warn!("Reap jobs thread terminated");
});
std::thread::spawn(move || {
let actix_runtime = actix_web::rt::System::new();
let http_server_handle = launch_http_server(create_server_args);
@@ -0,0 +1 @@
pub mod reap_jobs_thread;
@@ -0,0 +1,33 @@
use errors::AnyhowResult;
use log::{error, info};
use mysql_queries::queries::generic_inference::job::reap_stale_fakeyou_jobs::reap_stale_fakeyou_jobs;
use sqlx::MySqlPool;
use std::time::Duration;
const WAIT_BETWEEN_REAPING_SECONDS: u64 = 60 * 2; // 2 minutes
pub async fn reap_jobs_thread(
mysql_pool: MySqlPool,
) -> ! {
info!("Database job reaping thread starting...");
loop {
tokio::time::sleep(Duration::from_secs(WAIT_BETWEEN_REAPING_SECONDS)).await;
info!("Reaping jobs from database...");
let result = do_reap_jobs(&mysql_pool).await;
if let Err(err) = result {
error!("Error reaping jobs: {:?}", err);
tokio::time::sleep(Duration::from_secs(300)).await;
}
}
}
pub async fn do_reap_jobs(
mysql_pool: &MySqlPool,
) -> AnyhowResult<()> {
reap_stale_fakeyou_jobs(mysql_pool).await?;
Ok(())
}
@@ -1,55 +1,5 @@
use utoipa::OpenApi;
use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest;
use artcraft_api_defs::common::responses::job_details::JobDetailsLivePortraitRequest;
use artcraft_api_defs::common::responses::media_links::*;
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::*;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse;
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundRequest;
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProDuration;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterDuration;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProDuration;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteDuration;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteResolution;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2AspectRatio;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2Duration;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoResponse;
use artcraft_api_defs::jobs::list_session_jobs::*;
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_error::CreateCheckoutSessionError;
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_json_handler::*;
use billing_component::users::http_endpoints::list_active_user_subscriptions_handler::*;
use crate::http_server::common_requests::auto_product_category::AutoProductCategory;
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
use crate::http_server::common_responses::media::cover_image_links::CoverImageLinks;
@@ -96,40 +46,6 @@ use crate::http_server::endpoints::featured_items::delete_featured_item_handler:
use crate::http_server::endpoints::featured_items::get_is_featured_item_handler::*;
use crate::http_server::endpoints::image_studio::prompt::enqueue_studio_image_generation_handler::*;
use crate::http_server::endpoints::image_studio::upload::upload_snapshot_media_file_handler::*;
use enums::by_table::beta_keys::beta_key_product::BetaKeyProduct;
use enums::by_table::comments::comment_entity_type::CommentEntityType;
use enums::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
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_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_subtype::MediaFileSubtype;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::by_table::model_weights::{weights_category::WeightsCategory, weights_types::WeightsType};
use enums::by_table::prompts::prompt_type::PromptType;
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
use enums::by_table::user_bookmarks::user_bookmark_entity_type::UserBookmarkEntityType;
use enums::by_table::user_ratings::entity_type::UserRatingEntityType;
use enums::by_table::user_ratings::rating_value::UserRatingValue;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::visibility::Visibility;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use enums_public::by_table::media_files::public_media_file_model_type::PublicMediaFileModelType;
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
use tokens::tokens::batch_generations::*;
use tokens::tokens::beta_keys::*;
use tokens::tokens::browser_session_logs::*;
use tokens::tokens::comments::*;
use tokens::tokens::generic_inference_jobs::*;
use tokens::tokens::media_files::*;
use tokens::tokens::model_weights::*;
use tokens::tokens::prompts::*;
use tokens::tokens::users::*;
use tokens::tokens::user_bookmarks::*;
use tokens::tokens::zs_voice_datasets::*;
use crate::http_server::endpoints::inference_job::delete::dismiss_finished_session_jobs_handler::*;
use crate::http_server::endpoints::inference_job::delete::terminate_inference_job_handler::*;
use crate::http_server::endpoints::inference_job::get::batch_get_inference_job_status_handler::*;
@@ -160,7 +76,6 @@ use crate::http_server::endpoints::media_files::upload::upload_new_engine_asset_
use crate::http_server::endpoints::media_files::upload::upload_new_scene_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_pmx::upload_pmx_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_saved_scene_media_file_handler::*;
use crate::http_server::endpoints::generate::image::edit::gpt_image_1_edit_image_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_scene_snapshot_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_studio_shot::upload_studio_shot_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_video_new::upload_new_video_media_file_handler::*;
@@ -206,6 +121,98 @@ use crate::http_server::endpoints::weights::search::search_model_weights_impl::*
use crate::http_server::endpoints::weights::update::set_model_weight_cover_image_handler::*;
use crate::http_server::endpoints::weights::update::update_weight_handler::*;
use crate::http_server::web_utils::response_success_helpers::*;
use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest;
use artcraft_api_defs::common::responses::job_details::JobDetailsLivePortraitRequest;
use artcraft_api_defs::common::responses::media_links::*;
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::*;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageImageQuality;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageImageSize;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageNumImages;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageRequest;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageResponse;
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundRequest;
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProDuration;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterDuration;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProDuration;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteDuration;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteResolution;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2AspectRatio;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2Duration;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoResponse;
use artcraft_api_defs::jobs::list_session_jobs::*;
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_error::CreateCheckoutSessionError;
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_json_handler::*;
use billing_component::users::http_endpoints::list_active_user_subscriptions_handler::*;
use enums::by_table::beta_keys::beta_key_product::BetaKeyProduct;
use enums::by_table::comments::comment_entity_type::CommentEntityType;
use enums::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
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_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_subtype::MediaFileSubtype;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::by_table::model_weights::{weights_category::WeightsCategory, weights_types::WeightsType};
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::by_table::user_bookmarks::user_bookmark_entity_type::UserBookmarkEntityType;
use enums::by_table::user_ratings::entity_type::UserRatingEntityType;
use enums::by_table::user_ratings::rating_value::UserRatingValue;
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
use enums::common::generation_provider::GenerationProvider;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use enums_public::by_table::media_files::public_media_file_model_type::PublicMediaFileModelType;
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
use tokens::tokens::batch_generations::*;
use tokens::tokens::beta_keys::*;
use tokens::tokens::browser_session_logs::*;
use tokens::tokens::comments::*;
use tokens::tokens::generic_inference_jobs::*;
use tokens::tokens::media_files::*;
use tokens::tokens::model_weights::*;
use tokens::tokens::prompts::*;
use tokens::tokens::user_bookmarks::*;
use tokens::tokens::users::*;
use tokens::tokens::zs_voice_datasets::*;
#[derive(OpenApi)]
#[openapi(
@@ -235,6 +242,7 @@ use crate::http_server::web_utils::response_success_helpers::*;
crate::http_server::endpoints::generate::image::generate_flux_1_dev_text_to_image_handler::generate_flux_1_dev_text_to_image_handler,
crate::http_server::endpoints::generate::image::generate_flux_1_schnell_text_to_image_handler::generate_flux_1_schnell_text_to_image_handler,
crate::http_server::endpoints::generate::image::generate_flux_pro_11_text_to_image_handler::generate_flux_pro_11_text_to_image_handler,
crate::http_server::endpoints::generate::image::generate_gpt_image_1_text_to_image_handler::generate_gpt_image_1_text_to_image_handler,
crate::http_server::endpoints::generate::image::generate_flux_pro_11_ultra_text_to_image_handler::generate_flux_pro_11_ultra_text_to_image_handler,
crate::http_server::endpoints::generate::image::remove_image_background_handler::remove_image_background_handler,
crate::http_server::endpoints::generate::image::edit::gpt_image_1_edit_image_handler::gpt_image_1_edit_image_handler,
@@ -339,6 +347,7 @@ use crate::http_server::web_utils::response_success_helpers::*;
CommentEntityType,
FeaturedItemEntityType,
FrontendFailureCategory,
GenerationProvider,
InferenceCategory,
JobStatusPlus,
MediaFileAnimationType,
@@ -348,6 +357,8 @@ use crate::http_server::web_utils::response_success_helpers::*;
MediaFileOriginProductCategory,
MediaFileSubtype,
MediaFileType,
ModelType,
PromptContextSemanticType,
PromptType,
PublicMediaFileModelType,
PublicWeightsType,
@@ -415,12 +426,18 @@ use crate::http_server::web_utils::response_success_helpers::*;
BatchGetUserRatingError,
BatchGetUserRatingQueryParams,
BatchGetUserRatingResponse,
GenerateGptImage1TextToImageImageQuality,
GenerateGptImage1TextToImageImageSize,
GenerateGptImage1TextToImageNumImages,
GenerateGptImage1TextToImageRequest,
GenerateGptImage1TextToImageResponse,
BatchInferenceJobStatusResponsePayload,
BatchMediaFileInfo,
BatchRequestDetailsResponse,
BatchResultDetailsResponse,
BatchStatusDetailsResponse,
BetaKeyItem,
GetPromptImageContextItem,
BookmarkRow,
ByQueueStats,
ChangeMediaFileAnimationTypeError,
@@ -2,6 +2,7 @@ use actix_http::StatusCode;
use actix_web::{HttpResponse, HttpResponseBuilder, ResponseError};
use std::error::Error;
use std::fmt::{Display, Formatter};
use log::error;
#[derive(Debug)]
pub enum CommonWebError {
@@ -73,6 +74,13 @@ impl ResponseError for CommonWebError {
}
}
impl From<sqlx::Error> for CommonWebError {
fn from(value: sqlx::Error) -> Self {
error!("SQLx error: {:?}", value);
CommonWebError::ServerError
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -9,6 +9,9 @@ use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest, GptImage1EditImageResponse};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::enqueue_gpt_image_1_edit_image_webhook;
@@ -20,8 +23,12 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::batch_get_media_files_by_tokens;
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{InsertBatchArgs, PromptContextItem, insert_batch_prompt_context_items};
use sqlx::Acquire;
use utoipa::ToSchema;
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
/// Gpt Image 1 Image Editing
#[utoipa::path(
@@ -40,9 +47,13 @@ pub async fn gpt_image_1_edit_image_handler(
request: Json<GptImage1EditImageRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GptImage1EditImageResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -77,8 +88,8 @@ pub async fn gpt_image_1_edit_image_handler(
}
};
let result = batch_get_media_files_by_tokens(
&server_state.mysql_pool,
let result = batch_get_media_files_by_tokens_with_connection(
&mut mysql_connection,
tokens,
CAN_SEE_DELETED,
).await;
@@ -116,7 +127,7 @@ pub async fn gpt_image_1_edit_image_handler(
})
.collect::<Vec<_>>();
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -180,16 +191,69 @@ pub async fn gpt_image_1_edit_image_handler(
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::GptImage1),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
if let Some(token) = prompt_token.as_ref() {
let result = insert_batch_prompt_context_items(InsertBatchArgs {
prompt_token: token.clone(),
items: media_files.iter().map(|media_file| {
PromptContextItem {
media_token: media_file.token.clone(),
context_semantic_type: PromptContextSemanticType::Imgref,
}
}).collect(),
transaction: &mut transaction,
}).await;
if let Err(err) = result {
// NB: Fail open.
warn!("Error inserting batch prompt context items: {:?}", err);
}
}
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
@@ -199,6 +263,14 @@ pub async fn gpt_image_1_edit_image_handler(
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GptImage1EditImageResponse {
success: true,
@@ -6,6 +6,9 @@ use crate::state::server_state::ServerState;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageNumImages, GenerateFlux1DevTextToImageRequest, GenerateFlux1DevTextToImageResponse};
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::image::enqueue_flux_1_dev_text_to_image_webhook::enqueue_flux_1_dev_text_to_image_webhook;
use fal_client::requests::webhook::image::enqueue_flux_1_dev_text_to_image_webhook::{Flux1DevArgs, Flux1DevAspectRatio, Flux1DevNumImages};
@@ -15,6 +18,8 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use utoipa::ToSchema;
/// Flux 1 Dev text to image
@@ -34,9 +39,13 @@ pub async fn generate_flux_1_dev_text_to_image_handler(
request: Json<GenerateFlux1DevTextToImageRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateFlux1DevTextToImageResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -61,7 +70,7 @@ pub async fn generate_flux_1_dev_text_to_image_handler(
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -115,16 +124,51 @@ pub async fn generate_flux_1_dev_text_to_image_handler(
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::Flux1Dev),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
@@ -134,6 +178,14 @@ pub async fn generate_flux_1_dev_text_to_image_handler(
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GenerateFlux1DevTextToImageResponse {
success: true,
@@ -6,6 +6,9 @@ use crate::state::server_state::ServerState;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageNumImages, GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse};
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::image::enqueue_flux_1_schnell_text_to_image_webhook::enqueue_flux_1_schnell_text_to_image_webhook;
use fal_client::requests::webhook::image::enqueue_flux_1_schnell_text_to_image_webhook::{Flux1SchnellArgs, Flux1SchnellAspectRatio, Flux1SchnellNumImages};
@@ -15,6 +18,8 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use utoipa::ToSchema;
/// Flux 1 Schnell text to image
@@ -34,9 +39,13 @@ pub async fn generate_flux_1_schnell_text_to_image_handler(
request: Json<GenerateFlux1SchnellTextToImageRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateFlux1SchnellTextToImageResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -61,7 +70,7 @@ pub async fn generate_flux_1_schnell_text_to_image_handler(
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -114,17 +123,52 @@ pub async fn generate_flux_1_schnell_text_to_image_handler(
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::Flux1Schnell),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_prompt_token: prompt_token.as_ref(),
maybe_inference_args: None,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
@@ -134,6 +178,14 @@ pub async fn generate_flux_1_schnell_text_to_image_handler(
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GenerateFlux1SchnellTextToImageResponse {
success: true,
@@ -7,6 +7,9 @@ use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageNumImages, GenerateFluxPro11TextToImageRequest};
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::image::enqueue_flux_pro_11_text_to_image_webhook::FluxPro11Args;
use fal_client::requests::webhook::image::enqueue_flux_pro_11_text_to_image_webhook::{enqueue_flux_pro_11_text_to_image_webhook, FluxPro11AspectRatio, FluxPro11NumImages};
@@ -16,9 +19,10 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use utoipa::ToSchema;
/// Flux Pro 1.1
#[utoipa::path(
post,
@@ -36,9 +40,13 @@ pub async fn generate_flux_pro_11_text_to_image_handler(
request: Json<GenerateFluxPro11TextToImageRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateFluxPro11TextToImageResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -63,7 +71,7 @@ pub async fn generate_flux_pro_11_text_to_image_handler(
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -116,16 +124,51 @@ pub async fn generate_flux_pro_11_text_to_image_handler(
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::FluxPro11),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
@@ -136,6 +179,14 @@ pub async fn generate_flux_pro_11_text_to_image_handler(
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GenerateFluxPro11TextToImageResponse {
success: true,
inference_job_token: job_token,
@@ -7,6 +7,9 @@ use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageNumImages, GenerateFluxPro11UltraTextToImageRequest};
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::image::enqueue_flux_pro_11_ultra_text_to_image_webhook::FluxPro11UltraArgs;
use fal_client::requests::webhook::image::enqueue_flux_pro_11_ultra_text_to_image_webhook::{enqueue_flux_pro_11_ultra_text_to_image_webhook, FluxPro11UltraAspectRatio, FluxPro11UltraNumImages};
@@ -16,9 +19,10 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use utoipa::ToSchema;
/// Flux Pro 1.1 Ultra
#[utoipa::path(
post,
@@ -36,9 +40,13 @@ pub async fn generate_flux_pro_11_ultra_text_to_image_handler(
request: Json<GenerateFluxPro11UltraTextToImageRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateFluxPro11UltraTextToImageResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -63,7 +71,7 @@ pub async fn generate_flux_pro_11_ultra_text_to_image_handler(
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -116,16 +124,51 @@ pub async fn generate_flux_pro_11_ultra_text_to_image_handler(
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::FluxPro11Ultra),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
@@ -136,6 +179,14 @@ pub async fn generate_flux_pro_11_ultra_text_to_image_handler(
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GenerateFluxPro11UltraTextToImageResponse {
success: true,
inference_job_token: job_token,
@@ -0,0 +1,213 @@
use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest, GenerateGptImage1TextToImageResponse};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
use fal_client::requests::webhook::image::enqueue_gpt_image_1_text_to_image_webhook::{enqueue_gpt_image_1_text_to_image_webhook, GptTextToImageByokArgs, GptTextToImageNumImages, GptTextToImageQuality, GptTextToImageSize};
use http_server_common::request::get_request_ip::get_request_ip;
use log::{error, info, warn};
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use utoipa::ToSchema;
/// Gpt Image 1
#[utoipa::path(
post,
tag = "Generate Images",
path = "/v1/generate/image/gpt_image_1_text_to_image",
responses(
(status = 200, description = "Success", body = GenerateGptImage1TextToImageResponse),
),
params(
("request" = GenerateGptImage1TextToImageRequest, description = "Payload for Request"),
)
)]
pub async fn generate_gpt_image_1_text_to_image_handler(
http_request: HttpRequest,
request: Json<GenerateGptImage1TextToImageRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateGptImage1TextToImageResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
// Some(session) => session,
// None => {
// warn!("not logged in");
// return Err(CommonWebError::NotAuthorized);
// }
//};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
const CAN_SEE_DELETED: bool = false;
let media_domain = get_media_domain(&http_request);
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
})?;
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let image_size = match request.image_size {
Some(GenerateGptImage1TextToImageImageSize::Square) => GptTextToImageSize::Square,
Some(GenerateGptImage1TextToImageImageSize::Horizontal) => GptTextToImageSize::Horizontal,
Some(GenerateGptImage1TextToImageImageSize::Vertical) => GptTextToImageSize::Vertical,
None => GptTextToImageSize::Square, // Default to Square
};
let num_images = match request.num_images {
Some(GenerateGptImage1TextToImageNumImages::One) => GptTextToImageNumImages::One,
Some(GenerateGptImage1TextToImageNumImages::Two) => GptTextToImageNumImages::Two,
Some(GenerateGptImage1TextToImageNumImages::Three) => GptTextToImageNumImages::Three,
Some(GenerateGptImage1TextToImageNumImages::Four) => GptTextToImageNumImages::Four,
None => GptTextToImageNumImages::One, // Default to One
};
let quality = match request.image_quality {
Some(quality) => match quality {
GenerateGptImage1TextToImageImageQuality::Auto => GptTextToImageQuality::Auto,
GenerateGptImage1TextToImageImageQuality::Low => GptTextToImageQuality::Low,
GenerateGptImage1TextToImageImageQuality::Medium => GptTextToImageQuality::Medium,
GenerateGptImage1TextToImageImageQuality::High => GptTextToImageQuality::High,
},
None => GptTextToImageQuality::High, // Default to High
};
let openai_api_key = OpenAiApiKey::from_str(&server_state.openai.api_key);
let args = GptTextToImageByokArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
num_images,
image_size,
quality,
openai_api_key: &openai_api_key,
};
let fal_result = enqueue_gpt_image_1_text_to_image_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_gpt_image_1_text_to_image_webhook: {:?}", err);
CommonWebError::ServerError
})?;
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
CommonWebError::ServerError
})?;
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::GptImage1),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GenerateGptImage1TextToImageResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -3,4 +3,5 @@ pub mod generate_flux_1_dev_text_to_image_handler;
pub mod generate_flux_1_schnell_text_to_image_handler;
pub mod generate_flux_pro_11_text_to_image_handler;
pub mod generate_flux_pro_11_ultra_text_to_image_handler;
pub mod generate_gpt_image_1_text_to_image_handler;
pub mod remove_image_background_handler;
@@ -10,6 +10,7 @@ use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundRequest;
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundResponse;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::image::remove_background_rembg_webhook::{remove_background_rembg_webhook, RemoveBackgroundRembgWebhookArgs};
use http_server_common::request::get_request_ip::get_request_ip;
@@ -18,7 +19,9 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file_with_connection;
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use utoipa::ToSchema;
/// Background removal
@@ -38,9 +41,13 @@ pub async fn remove_image_background_handler(
request: Json<RemoveImageBackgroundRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<RemoveImageBackgroundResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -73,7 +80,7 @@ pub async fn remove_image_background_handler(
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -81,10 +88,10 @@ pub async fn remove_image_background_handler(
})?;
const IS_MOD : bool = false;
let media_file_lookup_result = get_media_file(
let media_file_lookup_result = get_media_file_with_connection(
media_file_token,
IS_MOD,
&server_state.mysql_pool,
&mut mysql_connection,
).await;
let media_file = match media_file_lookup_result {
@@ -145,11 +152,13 @@ pub async fn remove_image_background_handler(
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::BackgroundRemoval,
maybe_inference_args: None,
maybe_prompt_token: None,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
}).await;
let job_token = match db_result {
@@ -19,7 +19,7 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, get_media_file_with_connection};
use utoipa::ToSchema;
/// Hunyuan 2.0 Image to 3D
@@ -39,9 +39,13 @@ pub async fn generate_hunyuan_2_0_image_to_3d_handler(
request: Json<GenerateHunyuan20ImageTo3dRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateHunyuan20ImageTo3dResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -74,7 +78,7 @@ pub async fn generate_hunyuan_2_0_image_to_3d_handler(
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -82,10 +86,10 @@ pub async fn generate_hunyuan_2_0_image_to_3d_handler(
})?;
const IS_MOD : bool = false;
let media_file_lookup_result = get_media_file(
let media_file_lookup_result = get_media_file_with_connection(
media_file_token,
IS_MOD,
&server_state.mysql_pool,
&mut mysql_connection,
).await;
let media_file = match media_file_lookup_result {
@@ -146,11 +150,13 @@ pub async fn generate_hunyuan_2_0_image_to_3d_handler(
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ObjectGeneration,
maybe_inference_args: None,
maybe_prompt_token: None,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
}).await;
let job_token = match db_result {
@@ -19,7 +19,7 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, get_media_file_with_connection};
use utoipa::ToSchema;
/// Hunyuan 2.1 Image to 3D
@@ -39,9 +39,13 @@ pub async fn generate_hunyuan_2_1_image_to_3d_handler(
request: Json<GenerateHunyuan21ImageTo3dRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateHunyuan21ImageTo3dResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -74,7 +78,7 @@ pub async fn generate_hunyuan_2_1_image_to_3d_handler(
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -82,10 +86,10 @@ pub async fn generate_hunyuan_2_1_image_to_3d_handler(
})?;
const IS_MOD : bool = false;
let media_file_lookup_result = get_media_file(
let media_file_lookup_result = get_media_file_with_connection(
media_file_token,
IS_MOD,
&server_state.mysql_pool,
&mut mysql_connection,
).await;
let media_file = match media_file_lookup_result {
@@ -146,11 +150,13 @@ pub async fn generate_hunyuan_2_1_image_to_3d_handler(
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ObjectGeneration,
maybe_inference_args: None,
maybe_prompt_token: None,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
}).await;
let job_token = match db_result {
@@ -10,6 +10,9 @@ use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::{GenerateKling16ProAspectRatio, GenerateKling16ProImageToVideoRequest};
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::{GenerateKling16ProDuration, GenerateKling16ProImageToVideoResponse};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::video::enqueue_kling_16_pro_image_to_video_webhook::enqueue_kling_16_pro_image_to_video_webhook;
use fal_client::requests::webhook::video::enqueue_kling_16_pro_image_to_video_webhook::Kling16Duration;
@@ -21,30 +24,10 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file_with_connection;
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use utoipa::ToSchema;
/*
TODO: Either on the Tauri client or as an API, make "generic" image / video / object endpoints.
Take a reference image payload:
ReferenceImage {
media_token: MediaFileToken,
reference_type: ReferenceType
}
enum ReferenceType {
PrimaryReference,
CharacterReference,
LocationReference,
StyleReference,
...
GenericReference,
}
// **ORDER MATTERS**
reference_images: Vec<ReferenceImage>,
*/
/// Kling 1.6 Pro Image to Video
#[utoipa::path(
@@ -63,9 +46,13 @@ pub async fn generate_kling_1_6_pro_video_handler(
request: Json<GenerateKling16ProImageToVideoRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateKling16ProImageToVideoResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -98,7 +85,7 @@ pub async fn generate_kling_1_6_pro_video_handler(
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -106,10 +93,10 @@ pub async fn generate_kling_1_6_pro_video_handler(
})?;
const IS_MOD : bool = false;
let media_file_lookup_result = get_media_file(
let media_file_lookup_result = get_media_file_with_connection(
media_file_token,
IS_MOD,
&server_state.mysql_pool,
&mut mysql_connection,
).await;
let media_file = match media_file_lookup_result {
@@ -186,16 +173,51 @@ pub async fn generate_kling_1_6_pro_video_handler(
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::Kling16Pro),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: Some(prompt),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::VideoGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
@@ -206,6 +228,14 @@ pub async fn generate_kling_1_6_pro_video_handler(
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GenerateKling16ProImageToVideoResponse {
success: true,
inference_job_token: job_token,
@@ -10,6 +10,9 @@ use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::{GenerateKling21MasterAspectRatio, GenerateKling21MasterImageToVideoRequest};
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::{GenerateKling21MasterDuration, GenerateKling21MasterImageToVideoResponse};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::video::enqueue_kling_21_master_image_to_video_webhook::enqueue_kling_21_master_image_to_video_webhook;
use fal_client::requests::webhook::video::enqueue_kling_21_master_image_to_video_webhook::Kling21MasterArgs;
@@ -21,32 +24,11 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, get_media_file_with_connection};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use utoipa::ToSchema;
/*
TODO: Either on the Tauri client or as an API, make "generic" image / video / object endpoints.
Take a reference image payload:
ReferenceImage {
media_token: MediaFileToken,
reference_type: ReferenceType
}
enum ReferenceType {
PrimaryReference,
CharacterReference,
LocationReference,
StyleReference,
...
GenericReference,
}
// **ORDER MATTERS**
reference_images: Vec<ReferenceImage>,
*/
/// Kling 2.1 Master Image to Video
#[utoipa::path(
post,
@@ -64,9 +46,13 @@ pub async fn generate_kling_2_1_master_video_handler(
request: Json<GenerateKling21MasterImageToVideoRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateKling21MasterImageToVideoResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -99,7 +85,7 @@ pub async fn generate_kling_2_1_master_video_handler(
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -107,10 +93,10 @@ pub async fn generate_kling_2_1_master_video_handler(
})?;
const IS_MOD : bool = false;
let media_file_lookup_result = get_media_file(
let media_file_lookup_result = get_media_file_with_connection(
media_file_token,
IS_MOD,
&server_state.mysql_pool,
&mut mysql_connection,
).await;
let media_file = match media_file_lookup_result {
@@ -187,16 +173,51 @@ pub async fn generate_kling_2_1_master_video_handler(
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::Kling21Master),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: Some(prompt),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::VideoGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
@@ -10,6 +10,9 @@ use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::{GenerateKling21ProAspectRatio, GenerateKling21ProImageToVideoRequest};
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::{GenerateKling21ProDuration, GenerateKling21ProImageToVideoResponse};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::video::enqueue_kling_21_pro_image_to_video_webhook::enqueue_kling_21_pro_image_to_video_webhook;
use fal_client::requests::webhook::video::enqueue_kling_21_pro_image_to_video_webhook::Kling21ProArgs;
@@ -21,32 +24,11 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, get_media_file_with_connection};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use utoipa::ToSchema;
/*
TODO: Either on the Tauri client or as an API, make "generic" image / video / object endpoints.
Take a reference image payload:
ReferenceImage {
media_token: MediaFileToken,
reference_type: ReferenceType
}
enum ReferenceType {
PrimaryReference,
CharacterReference,
LocationReference,
StyleReference,
...
GenericReference,
}
// **ORDER MATTERS**
reference_images: Vec<ReferenceImage>,
*/
/// Kling 2.1 Pro Image to Video
#[utoipa::path(
post,
@@ -64,9 +46,13 @@ pub async fn generate_kling_2_1_pro_video_handler(
request: Json<GenerateKling21ProImageToVideoRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateKling21ProImageToVideoResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -99,7 +85,7 @@ pub async fn generate_kling_2_1_pro_video_handler(
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -107,10 +93,10 @@ pub async fn generate_kling_2_1_pro_video_handler(
})?;
const IS_MOD : bool = false;
let media_file_lookup_result = get_media_file(
let media_file_lookup_result = get_media_file_with_connection(
media_file_token,
IS_MOD,
&server_state.mysql_pool,
&mut mysql_connection,
).await;
let media_file = match media_file_lookup_result {
@@ -187,16 +173,51 @@ pub async fn generate_kling_2_1_pro_video_handler(
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::Kling21Pro),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: Some(prompt),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::VideoGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
@@ -207,6 +228,14 @@ pub async fn generate_kling_2_1_pro_video_handler(
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GenerateKling21ProImageToVideoResponse {
success: true,
inference_job_token: job_token,
@@ -12,6 +12,9 @@ use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_vide
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteResolution;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::video::enqueue_seedance_1_lite_image_to_video_webhook::enqueue_seedance_1_lite_image_to_video_webhook;
use fal_client::requests::webhook::video::enqueue_seedance_1_lite_image_to_video_webhook::Seedance1LiteDuration;
@@ -22,7 +25,9 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file, get_media_file_with_connection};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use utoipa::ToSchema;
/// Seedance 1.0 Lite Image to Video
@@ -42,9 +47,13 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
request: Json<GenerateSeedance10LiteImageToVideoRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateSeedance10LiteImageToVideoResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -77,7 +86,7 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -85,10 +94,10 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
})?;
const IS_MOD : bool = false;
let media_file_lookup_result = get_media_file(
let media_file_lookup_result = get_media_file_with_connection(
media_file_token,
IS_MOD,
&server_state.mysql_pool,
&mut mysql_connection,
).await;
let media_file = match media_file_lookup_result {
@@ -166,16 +175,51 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::Seedance10Lite),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: Some(prompt),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::VideoGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
@@ -185,6 +229,14 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GenerateSeedance10LiteImageToVideoResponse {
success: true,
@@ -2,16 +2,21 @@ use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::analytics::log_browser_session_handler::LogBrowserSessionError;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use anyhow::anyhow;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2AspectRatio;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2Duration;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoResponse;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::enqueue_veo_2_image_to_video_webhook;
use fal_client::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::Veo2Args;
@@ -23,7 +28,10 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file_with_connection;
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use tokens::tokens::prompts::PromptToken;
use utoipa::ToSchema;
/// Veo 2 Image to Video
@@ -43,9 +51,13 @@ pub async fn generate_veo_2_image_to_video_handler(
request: Json<GenerateVeo2ImageToVideoRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateVeo2ImageToVideoResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -78,7 +90,7 @@ pub async fn generate_veo_2_image_to_video_handler(
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -86,10 +98,10 @@ pub async fn generate_veo_2_image_to_video_handler(
})?;
const IS_MOD : bool = false;
let media_file_lookup_result = get_media_file(
let media_file_lookup_result = get_media_file_with_connection(
media_file_token,
IS_MOD,
&server_state.mysql_pool,
&mut mysql_connection,
).await;
let media_file = match media_file_lookup_result {
@@ -169,16 +181,53 @@ pub async fn generate_veo_2_image_to_video_handler(
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::Veo2),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: Some(prompt),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::VideoGeneration,
maybe_inference_args: None,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool: &server_state.mysql_pool,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
@@ -189,6 +238,14 @@ pub async fn generate_veo_2_image_to_video_handler(
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GenerateVeo2ImageToVideoResponse {
success: true,
inference_job_token: job_token,
@@ -1,22 +1,28 @@
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Path;
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
use artcraft_api_defs::common::responses::media_links::MediaLinks;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use chrono::{DateTime, Utc};
use log::warn;
use utoipa::ToSchema;
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use mysql_queries::queries::prompts::get_prompt::get_prompt;
use log::{error, warn};
use mysql_queries::queries::prompt_context_items::list_prompt_context_items::list_prompt_context_items;
use mysql_queries::queries::prompts::get_prompt::{get_prompt, get_prompt_from_connection};
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use utoipa::ToSchema;
/// For the URL PathInfo
#[derive(Deserialize, ToSchema)]
@@ -30,6 +36,13 @@ pub struct GetPromptSuccessResponse {
pub prompt: PromptInfo,
}
#[derive(Serialize, ToSchema)]
pub struct GetPromptImageContextItem {
pub media_token: MediaFileToken,
pub semantic: PromptContextSemanticType,
pub media_links: MediaLinks,
}
#[derive(Serialize, ToSchema)]
pub struct PromptInfo {
pub token: PromptToken,
@@ -37,6 +50,12 @@ pub struct PromptInfo {
/// The type of prompt.
/// Note: Prompts may or may not be compatible across systems.
pub prompt_type: PromptType,
/// The type of model used
pub maybe_model_type: Option<ModelType>,
/// The service provider used
pub maybe_generation_provider: Option<GenerationProvider>,
/// Positive prompt (technically optional, but usually present)
pub maybe_positive_prompt: Option<String>,
@@ -44,6 +63,9 @@ pub struct PromptInfo {
/// Negative prompt (optional)
pub maybe_negative_prompt: Option<String>,
/// Context images (optional)
pub maybe_context_images: Option<Vec<GetPromptImageContextItem>>,
/// Scheduled / travel prompt (optional)
pub maybe_travel_prompt: Option<String>,
@@ -165,9 +187,17 @@ pub async fn get_prompt_handler(
path: Path<GetPromptPathInfo>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, GetPromptError>
{
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
error!("Error acquiring MySQL connection: {:?}", err);
GetPromptError::ServerError
})?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -180,9 +210,9 @@ pub async fn get_prompt_handler(
let prompt_token = path.into_inner().token;
let result = get_prompt(
let result = get_prompt_from_connection(
&prompt_token,
&server_state.mysql_pool
&mut mysql_connection
).await;
let result = match result {
@@ -247,13 +277,64 @@ pub async fn get_prompt_handler(
});
}
let media_domain = get_media_domain(&http_request);
let items_result = list_prompt_context_items(
&result.token,
&mut mysql_connection
).await;
let items = items_result.unwrap_or_else(|e| {
warn!("Error listing prompt context items: {:?}", e);
Vec::new()
});
let items = items.iter().filter_map(|item| {
match item.context_semantic_type {
PromptContextSemanticType::VidStartFrame => {},
PromptContextSemanticType::VidEndFrame => {},
PromptContextSemanticType::Imgref => {},
PromptContextSemanticType::ImgrefCharacter => {},
PromptContextSemanticType::ImgrefStyle => {},
PromptContextSemanticType::ImgrefBg => {},
_ => {
// NB: Only return images. In the future we may add context items for stages, persisted data, etc.
return None
},
}
let bucket_path = MediaFileBucketPath::from_object_hash(
&item.public_bucket_directory_hash,
item.maybe_public_bucket_prefix.as_deref(),
item.maybe_public_bucket_extension.as_deref());
Some(GetPromptImageContextItem {
media_token: item.media_token.clone(),
semantic: item.context_semantic_type,
media_links: MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&bucket_path,
),
})
}).collect::<Vec<GetPromptImageContextItem>>();
let maybe_context_images = if items.is_empty() {
None
} else {
Some(items)
};
let response = GetPromptSuccessResponse {
success: true,
prompt: PromptInfo {
token: result.token,
maybe_strength,
maybe_model_type: result.maybe_model_type,
maybe_generation_provider: result.maybe_generation_provider,
maybe_positive_prompt: result.maybe_positive_prompt,
maybe_negative_prompt: result.maybe_negative_prompt,
maybe_context_images,
maybe_travel_prompt,
maybe_style_name,
maybe_inference_duration_millis,
@@ -7,8 +7,10 @@ use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCat
use enums::by_table::media_files::media_file_type::MediaFileType;
use errors::AnyhowResult;
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
use images::encoding::webp_bytes_to_png_bytes::webp_bytes_to_png_bytes;
use images::image_info::image_info::ImageInfo;
use log::info;
use mimetypes::mimetype_info::file_extension::FileExtension;
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
@@ -32,18 +34,17 @@ pub async fn handle_image_payload(
job: &FalJobDetails,
server_state: &ServerState,
) -> AnyhowResult<MediaFileToken> {
let image_value = payload.get("image")
.ok_or_else(|| anyhow!("no `image` key in payload"))?;
info!("Fal Image Payload: {:?}", image_value);
let image: FalWebhookImage = serde_json::from_value(image_value.clone())?;
let image_url = image.url
.as_deref()
.ok_or_else(|| anyhow!("no `url` in image payload"))?;
//let mime_type = image.content_type
// .as_deref()
// .ok_or_else(|| anyhow!("no `content_type` in image payload"))?;
@@ -51,10 +52,53 @@ pub async fn handle_image_payload(
let file_bytes = http_download_url_to_bytes(image_url)
.await
.map_err(|e| anyhow!("Failed to download image: {:?}", e))?;
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
info!("File type: {}, extension: {:?}",
mimetype_info.mime_type(),
mimetype_info.file_extension());
match mimetype_info.file_extension() {
Some(FileExtension::Webp) => {
info!("Artcraft can't handle WebP images yet; converting to PNG...");
let file_bytes = webp_bytes_to_png_bytes(&file_bytes)?;
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
info!("Updated file type: {}, extension: {:?}",
mimetype_info.mime_type(),
mimetype_info.file_extension());
upload_single_image_bytes(
job,
server_state,
&file_bytes,
mimetype_info,
).await
}
_ => {
upload_single_image_bytes(
job,
server_state,
&file_bytes,
mimetype_info,
).await
}
}
}
async fn upload_single_image_bytes(
job: &FalJobDetails,
server_state: &ServerState,
file_bytes: &[u8],
mimetype_info: MimetypeInfo,
) -> AnyhowResult<MediaFileToken> {
let mime_type = mimetype_info.mime_type();
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
@@ -86,6 +130,7 @@ pub async fn handle_image_payload(
.media_file_class(MediaFileClass::Image)
.media_file_type(media_file_type)
.media_file_origin_category(MediaFileOriginCategory::Inference)
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
//.media_file_origin_product_category(MediaFileOriginProductCategory::Unknown)
.mime_type(mime_type)
.file_size_bytes(file_size_bytes as u64)
@@ -7,8 +7,10 @@ use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCat
use enums::by_table::media_files::media_file_type::MediaFileType;
use errors::AnyhowResult;
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
use images::encoding::webp_bytes_to_png_bytes::webp_bytes_to_png_bytes;
use images::image_info::image_info::ImageInfo;
use log::info;
use mimetypes::mimetype_info::file_extension::FileExtension;
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
@@ -53,7 +55,7 @@ pub async fn handle_images_payload(
Ok(maybe_media_token)
}
pub async fn upload_image(
async fn upload_image(
job: &FalJobDetails,
server_state: &ServerState,
image: &FalWebhookImage,
@@ -73,11 +75,58 @@ pub async fn upload_image(
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
info!("File type: {}, extension: {:?}",
mimetype_info.mime_type(),
mimetype_info.file_extension());
match mimetype_info.file_extension() {
Some(FileExtension::Webp) => {
info!("Artcraft can't handle WebP images yet; converting to PNG...");
let file_bytes = webp_bytes_to_png_bytes(&file_bytes)?;
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
info!("Updated file type: {}, extension: {:?}",
mimetype_info.mime_type(),
mimetype_info.file_extension());
upload_image_bytes(
job,
server_state,
image,
&file_bytes,
mimetype_info,
).await
}
_ => {
upload_image_bytes(
job,
server_state,
image,
&file_bytes,
mimetype_info,
).await
}
}
}
async fn upload_image_bytes(
job: &FalJobDetails,
server_state: &ServerState,
image: &FalWebhookImage,
file_bytes: &[u8],
mimetype_info: MimetypeInfo,
) -> AnyhowResult<MediaFileToken> {
let mime_type = mimetype_info.mime_type();
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
.ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?;
info!("MediaFileType: {:?}", media_file_type);
let extension_with_period = mimetype_info.file_extension()
.map(|ext| ext.extension_with_period())
.ok_or_else(|| anyhow!("Failed to get file extension from mimetype info"))?;
@@ -109,6 +158,7 @@ pub async fn upload_image(
.file_size_bytes(file_size_bytes as u64)
.frame_width(image_info.width())
.frame_height(image_info.height())
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
.checksum_sha2(&file_hash)
.insert_pool(&server_state.mysql_pool)
.await?;
@@ -116,4 +166,4 @@ pub async fn upload_image(
info!("Image media file uploaded with token: {}", media_token);
Ok(media_token)
}
}

Some files were not shown because too many files have changed in this diff Show More