mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-01 00:05:54 -04:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0058dc28c | ||
|
|
af02cc6525 | ||
|
|
4f0916edb7 | ||
|
|
c36e88d8ce | ||
|
|
233f60acf6 | ||
|
|
a3e16e491a | ||
|
|
3d521fec79 | ||
|
|
6c8979123f | ||
|
|
8d4edcfafd | ||
|
|
b53e138dab | ||
|
|
dc1b038ffa | ||
|
|
418a9f92c0 | ||
|
|
d671f7f951 | ||
|
|
e97e5c239e | ||
|
|
a7270f4685 | ||
|
|
a6fb661fbe | ||
|
|
e5f5a06b6b |
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n \n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 13
|
||||
"Right": 14
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0fa824822cd94024dce21243f41e69e4a3e8685c4de676834a7c0ee14982c976"
|
||||
"hash": "002cdb3d086f3b4181e2997befa54bd8516cb1d3d3bb6e389862695318d5c299"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO prompts\nSET\n token = ?,\n prompt_type = ?,\n\n maybe_creator_user_token = ?,\n \n maybe_model_type = ?,\n maybe_generation_provider = ?,\n\n maybe_positive_prompt = ?,\n maybe_negative_prompt = ?,\n \n maybe_other_args = ?,\n\n creator_ip_address = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 9
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "08f52f15acf6afc7091a525e3dc88f706c71c3579e5e81aa9903807d85f469ef"
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n pci.media_token as `media_token: tokens::tokens::media_files::MediaFileToken`,\n pci.context_semantic_type as `context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension\n\nFROM prompt_context_items pci\nJOIN media_files m\nON\n pci.media_token = m.token\nWHERE\n pci.prompt_token = ?\nORDER BY pci.id ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "media_token: tokens::tokens::media_files::MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "public_bucket_directory_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "094eb099cde85835db0662c2700c6b82644b51bf9b3609391ed4d2945fe5e98f"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO prompts\nSET\n token = ?,\n prompt_type = ?,\n\n maybe_creator_user_token = ?,\n\n maybe_positive_prompt = ?,\n maybe_negative_prompt = ?,\n maybe_other_args = ?,\n\n creator_ip_address = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "13ca06bcbb4a576f8a2229a8554b7488d379baa381d8d5cb60972750e81b2173"
|
||||
}
|
||||
+29
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n p.token as `token: tokens::tokens::prompts::PromptToken`,\n\n p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,\n\n p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n\n p.maybe_positive_prompt,\n p.maybe_negative_prompt,\n p.maybe_other_args,\n\n p.creator_ip_address,\n p.created_at\n\nFROM prompts as p\nWHERE\n p.token = ?\n ",
|
||||
"query": "\nSELECT\n p.token as `token: tokens::tokens::prompts::PromptToken`,\n\n p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,\n\n p.maybe_model_type as `maybe_model_type: enums::common::model_type::ModelType`,\n p.maybe_generation_provider as `maybe_generation_provider: enums::common::generation_provider::GenerationProvider`,\n\n p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n\n p.maybe_positive_prompt,\n p.maybe_negative_prompt,\n p.maybe_other_args,\n\n p.creator_ip_address,\n p.created_at\n\nFROM prompts as p\nWHERE\n p.token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -25,6 +25,26 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "maybe_model_type: enums::common::model_type::ModelType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_generation_provider: enums::common::generation_provider::GenerationProvider",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -34,7 +54,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"ordinal": 5,
|
||||
"name": "maybe_positive_prompt",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
@@ -44,7 +64,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 6,
|
||||
"name": "maybe_negative_prompt",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
@@ -54,7 +74,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 7,
|
||||
"name": "maybe_other_args",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
@@ -64,7 +84,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 8,
|
||||
"name": "creator_ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -74,7 +94,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 9,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -94,9 +114,11 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "61744bbf83f7cf00348bcc21e868d9c506fec46da4a33ac50d63c6d9e00c0cec"
|
||||
"hash": "b46b0d022790f4e10f6e47126df0fdc55172bf8cda77d4e0b2afac9e6ebd1630"
|
||||
}
|
||||
+14
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n \n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -75,6 +75,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -84,7 +94,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -105,9 +115,10 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "64dfacc1f0552d539f009fea91e902a2fe296c06ad539808596bbf2c8fc9eaa0"
|
||||
"hash": "fbe482c8987231c3b5d75f2e792d0fdd033de8a7eb821428fa034752c73edfb7"
|
||||
}
|
||||
@@ -9,9 +9,6 @@ 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.
|
||||
@@ -22,6 +19,9 @@ CREATE TABLE prompt_context_items (
|
||||
-- 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),
|
||||
|
||||
@@ -30,7 +30,7 @@ CREATE TABLE prompts (
|
||||
maybe_model_type VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- What service provided the generation.
|
||||
maybe_generation_provider VARCHAR(16) NOT NULL, -- TODO
|
||||
maybe_generation_provider VARCHAR(16) NOT NULL,
|
||||
|
||||
-- Whoever created the prompt
|
||||
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::prelude::fal_ai::gpt_image_1::edit_image::byok::{gpt_text_to_image, GptTextToImageRequest};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct GptTextToImageByokArgs<'a, V: IntoUrl> {
|
||||
// Request
|
||||
pub prompt: &'a str,
|
||||
pub image_size: GptTextToImageSize,
|
||||
pub num_images: GptTextToImageNumImages,
|
||||
pub quality: GptTextToImageQuality,
|
||||
|
||||
// Fulfillment
|
||||
pub api_key: &'a FalApiKey,
|
||||
pub openai_api_key: &'a OpenAiApiKey,
|
||||
pub webhook_url: V,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptTextToImageSize{
|
||||
Auto,
|
||||
Square,
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptTextToImageQuality {
|
||||
Auto,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptTextToImageNumImages{
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
pub async fn enqueue_gpt_image_1_text_to_image_webhook<V: IntoUrl>(
|
||||
args: GptTextToImageByokArgs<'_, V>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
// auto, 1024x1024, 1536x1024, 1024x1536
|
||||
let image_size = match args.image_size {
|
||||
GptTextToImageSize::Auto => "auto",
|
||||
GptTextToImageSize::Square => "1024x1024",
|
||||
GptTextToImageSize::Horizontal => "1536x1024",
|
||||
GptTextToImageSize::Vertical => "1024x1536",
|
||||
};
|
||||
|
||||
let quality = match args.quality {
|
||||
GptTextToImageQuality::Auto => "auto",
|
||||
GptTextToImageQuality::Low => "low",
|
||||
GptTextToImageQuality::Medium => "medium",
|
||||
GptTextToImageQuality::High => "high",
|
||||
};
|
||||
|
||||
let num_images = match args.num_images {
|
||||
GptTextToImageNumImages::One => 1,
|
||||
GptTextToImageNumImages::Two => 2,
|
||||
GptTextToImageNumImages::Three => 3,
|
||||
GptTextToImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let request = GptTextToImageRequest {
|
||||
prompt: args.prompt.to_string(),
|
||||
image_size: image_size.to_string(),
|
||||
num_images,
|
||||
quality: quality.to_string(),
|
||||
openai_api_key: args.openai_api_key.0.to_string(),
|
||||
};
|
||||
|
||||
let result = gpt_text_to_image(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use crate::requests::webhook::image::enqueue_gpt_image_1_text_to_image_webhook::{enqueue_gpt_image_1_text_to_image_webhook, GptTextToImageByokArgs, GptTextToImageNumImages, GptTextToImageQuality, GptTextToImageSize};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_gpt_image_1_edit_image() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let fal_api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/openai_api_key.txt")?;
|
||||
|
||||
let open_ai_api_key = OpenAiApiKey::from_str(&secret);
|
||||
|
||||
let args = GptTextToImageByokArgs {
|
||||
prompt: "put the man and the ghost on the grassy hill. the man is scared of the friendly ghost.",
|
||||
api_key: &fal_api_key,
|
||||
openai_api_key: &open_ai_api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
image_size: GptTextToImageSize::Horizontal,
|
||||
num_images: GptTextToImageNumImages::One,
|
||||
quality: GptTextToImageQuality::High,
|
||||
};
|
||||
|
||||
let result = enqueue_gpt_image_1_text_to_image_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageRequest, GenerateGptImage1TextToImageResponse, GPT_IMAGE_1_TEXT_TO_IMAGE_PATH};
|
||||
|
||||
|
||||
pub async fn generate_gpt_image_1_text_to_image(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateGptImage1TextToImageRequest,
|
||||
) -> Result<GenerateGptImage1TextToImageResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GPT_IMAGE_1_TEXT_TO_IMAGE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -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;
|
||||
+15
-2
@@ -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
|
||||
|
||||
+69
-45
@@ -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,
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
|
||||
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use log::{error, info};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use std::time::Duration;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_gpt_image_1(
|
||||
request: EnqueueTextToImageRequest,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, InternalImageError> {
|
||||
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
// TODO: Check if providers are available before proceeding.
|
||||
|
||||
info!("Providers by priority: {:?}", priority);
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Fal => {
|
||||
// Fallthrough
|
||||
// If in the future we support OpenAI API keys, it's worth considering whether we
|
||||
// send those requests ourselves, or if we use FAL as an intermediary. FAL makes
|
||||
// the API nicer to deal with, but the user needs an additional key.
|
||||
}
|
||||
Provider::Artcraft => {
|
||||
info!("Dispatching gpt-image-1 via Artcraft...");
|
||||
return handle_gpt_image_1_artcraft(
|
||||
request,
|
||||
app_env_configs,
|
||||
storyteller_creds_manager
|
||||
).await;
|
||||
}
|
||||
Provider::Sora => {
|
||||
info!("Dispatching gpt-image-1 via Sora...");
|
||||
return handle_gpt_image_1_sora(
|
||||
request,
|
||||
app,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(InternalImageError::NoProviderAvailable)
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize};
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use std::time::Duration;
|
||||
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
|
||||
use storyteller_client::generate::image::generate_gpt_image_1_text_to_image::generate_gpt_image_1_text_to_image;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_gpt_image_1_artcraft(
|
||||
request: EnqueueTextToImageRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, InternalImageError> {
|
||||
|
||||
let creds = match storyteller_creds_manager.get_credentials()? {
|
||||
Some(creds) => creds,
|
||||
None => {
|
||||
return Err(InternalImageError::NeedsStorytellerCredentials);
|
||||
},
|
||||
};
|
||||
|
||||
info!("Calling Artcraft gpt-image-1 ...");
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let image_quality = Some(GenerateGptImage1TextToImageImageQuality::High);
|
||||
|
||||
//let image_quality = request.image_quality
|
||||
// .map(|quality| match quality {
|
||||
// EditImageQuality::Auto => GenerateGptImage1TextToImageImageQuality::Auto,
|
||||
// EditImageQuality::High => GenerateGptImage1TextToImageImageQuality::High,
|
||||
// EditImageQuality::Medium => GenerateGptImage1TextToImageImageQuality::Medium,
|
||||
// EditImageQuality::Low => GenerateGptImage1TextToImageImageQuality::Low,
|
||||
// });
|
||||
|
||||
let image_size = request.aspect_ratio
|
||||
.map(|size| match size {
|
||||
TextToImageSize::Auto => GenerateGptImage1TextToImageImageSize::Square,
|
||||
TextToImageSize::Square => GenerateGptImage1TextToImageImageSize::Square,
|
||||
TextToImageSize::Tall => GenerateGptImage1TextToImageImageSize::Vertical,
|
||||
TextToImageSize::Wide => GenerateGptImage1TextToImageImageSize::Horizontal,
|
||||
});
|
||||
|
||||
let num_images = match request.number_images {
|
||||
None => None,
|
||||
Some(1) => Some(GenerateGptImage1TextToImageNumImages::One),
|
||||
Some(2) => Some(GenerateGptImage1TextToImageNumImages::Two),
|
||||
Some(3) => Some(GenerateGptImage1TextToImageNumImages::Three),
|
||||
Some(4) => Some(GenerateGptImage1TextToImageNumImages::Four),
|
||||
// TODO: Error
|
||||
//Some(other) => {
|
||||
// return Err(InternalImageError::InvalidNumberOfRequestedImages {
|
||||
// min: 1,
|
||||
// max: 4,
|
||||
// requested: other,
|
||||
// });
|
||||
//},
|
||||
_ => Some(GenerateGptImage1TextToImageNumImages::One), // Default to one image if invalid number
|
||||
};
|
||||
|
||||
let request = GenerateGptImage1TextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt.clone(),
|
||||
image_size,
|
||||
num_images,
|
||||
image_quality,
|
||||
};
|
||||
|
||||
let result = generate_gpt_image_1_text_to_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
|
||||
let job_id = match result {
|
||||
Ok(enqueued) => {
|
||||
// TODO(bt,2025-07-05): Enqueue job token?
|
||||
info!("Successfully enqueued Artcraft gpt-image-1. Job token: {}",
|
||||
enqueued.inference_job_token);
|
||||
enqueued.inference_job_token
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
|
||||
return Err(InternalImageError::StorytellerError(err));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
provider: GenerationProvider::Artcraft,
|
||||
model: Some(GenerationModel::GptImage1),
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
})
|
||||
}
|
||||
+12
-3
@@ -19,9 +19,9 @@ use tauri::AppHandle;
|
||||
|
||||
const SORA_SIMPLE_IMAGE_GEN_TIMEOUT : Duration = Duration::from_millis(1000 * 30); // 30 seconds
|
||||
|
||||
pub async fn handle_image_sora(
|
||||
app: &AppHandle,
|
||||
pub async fn handle_gpt_image_1_sora(
|
||||
request: EnqueueTextToImageRequest,
|
||||
app: &AppHandle,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, InternalImageError> {
|
||||
@@ -55,10 +55,19 @@ pub async fn handle_image_sora(
|
||||
Some(TextToImageSize::Wide) => ImageSize::Wide,
|
||||
};
|
||||
|
||||
let num_images = match request.number_images {
|
||||
None => NumImages::One,
|
||||
Some(1) => NumImages::One,
|
||||
Some(2) => NumImages::Two,
|
||||
Some(3) => NumImages::Two, // NB: There is no option for three images!
|
||||
Some(4) => NumImages::Four,
|
||||
_ => NumImages::One,
|
||||
};
|
||||
|
||||
let result =
|
||||
simple_image_gen_with_session_auto_renew(SimpleImageGenAutoRenewRequest {
|
||||
prompt: prompt.to_string(),
|
||||
num_images: NumImages::One,
|
||||
num_images,
|
||||
image_size: size,
|
||||
credentials: &creds,
|
||||
request_timeout: Some(SORA_SIMPLE_IMAGE_GEN_TIMEOUT),
|
||||
@@ -1 +1,3 @@
|
||||
pub mod handle_image_sora;
|
||||
pub (crate) mod handle_gpt_image_1_artcraft;
|
||||
pub (crate) mod handle_gpt_image_1_sora;
|
||||
pub (super) mod handle_gpt_image_1;
|
||||
|
||||
+1
-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};
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@ use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg
|
||||
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal_fal::handle_generic_bg_removal_fal;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
@@ -16,7 +16,7 @@ use tauri::AppHandle;
|
||||
|
||||
pub(super) const MAX_IMAGES: usize = 10;
|
||||
|
||||
pub async fn handle_gpt_image_1(
|
||||
pub async fn handle_gpt_image_1_edit(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
@@ -44,8 +44,8 @@ pub async fn handle_gpt_image_1(
|
||||
// the API nicer to deal with, but the user needs an additional key.
|
||||
}
|
||||
Provider::Artcraft => {
|
||||
info!("Dispatching gpt-image-1 via Artcraft...");
|
||||
return handle_gpt_image_1_artcraft(
|
||||
info!("Dispatching gpt-image-1 (edit) via Artcraft...");
|
||||
return handle_gpt_image_1_edit_artcraft(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
@@ -54,8 +54,8 @@ pub async fn handle_gpt_image_1(
|
||||
).await;
|
||||
}
|
||||
Provider::Sora => {
|
||||
info!("Dispatching gpt-image-1 via Sora...");
|
||||
return handle_gpt_image_1_sora(
|
||||
info!("Dispatching gpt-image-1 (edit) via Sora...");
|
||||
return handle_gpt_image_1_edit_sora(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
+3
-3
@@ -2,7 +2,7 @@ use crate::core::commands::enqueue::image::enqueue_text_to_image_command::Enqueu
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::MAX_IMAGES;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::MAX_IMAGES;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
@@ -36,7 +36,7 @@ use tauri::AppHandle;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
|
||||
pub async fn handle_gpt_image_1_artcraft(
|
||||
pub async fn handle_gpt_image_1_edit_artcraft(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
@@ -51,7 +51,7 @@ pub async fn handle_gpt_image_1_artcraft(
|
||||
},
|
||||
};
|
||||
|
||||
info!("Calling Artcraft gpt-image-1 ...");
|
||||
info!("Calling Artcraft gpt-image-1 (edit) ...");
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ use crate::core::commands::enqueue::image::enqueue_text_to_image_command::Enqueu
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageSize, EnqueueContextualEditImageCommand, EnqueueContextualEditImageErrorType};
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::MAX_IMAGES;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::MAX_IMAGES;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
@@ -48,7 +48,7 @@ const SORA_IMAGE_REMIX_TIMEOUT: Duration = Duration::from_millis(1000 * 30); //
|
||||
|
||||
const DEFAULT_ASPECT_RATIO : EditImageSize = EditImageSize::Square;
|
||||
|
||||
pub async fn handle_gpt_image_1_sora(
|
||||
pub async fn handle_gpt_image_1_edit_sora(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod handle_gpt_image_1;
|
||||
pub mod handle_gpt_image_1_artcraft;
|
||||
pub mod handle_gpt_image_1_sora;
|
||||
pub mod handle_gpt_image_1_edit;
|
||||
pub mod handle_gpt_image_1_edit_artcraft;
|
||||
pub mod handle_gpt_image_1_edit_sora;
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+58
@@ -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))
|
||||
},
|
||||
}
|
||||
}
|
||||
+75
@@ -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
|
||||
}
|
||||
|
||||
+58
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
+8
@@ -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::*;
|
||||
|
||||
+21
-5
@@ -25,8 +25,10 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
|
||||
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::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(
|
||||
@@ -47,11 +49,7 @@ pub async fn gpt_image_1_edit_image_handler(
|
||||
) -> Result<Json<GptImage1EditImageResponse>, CommonWebError> {
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("MySql pool error: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
.await?;
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
@@ -225,6 +223,24 @@ pub async fn gpt_image_1_edit_image_handler(
|
||||
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,
|
||||
|
||||
+54
-4
@@ -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,17 +124,50 @@ 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: 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: &server_state.mysql_pool,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
@@ -136,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,
|
||||
|
||||
+54
-4
@@ -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,18 +123,51 @@ 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: None,
|
||||
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_executor: &server_state.mysql_pool,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
@@ -136,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,
|
||||
|
||||
+54
-5
@@ -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,17 +124,50 @@ 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: 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: &server_state.mysql_pool,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
@@ -138,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,
|
||||
|
||||
+54
-5
@@ -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,17 +124,50 @@ 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: 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: &server_state.mysql_pool,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
@@ -138,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,
|
||||
|
||||
+213
@@ -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;
|
||||
+13
-6
@@ -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 {
|
||||
@@ -150,7 +157,7 @@ pub async fn remove_image_background_handler(
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
mysql_executor: &mut *mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+10
-6
@@ -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 {
|
||||
@@ -151,7 +155,7 @@ pub async fn generate_hunyuan_2_0_image_to_3d_handler(
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
mysql_executor: &mut *mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+10
-6
@@ -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 {
|
||||
@@ -151,7 +155,7 @@ pub async fn generate_hunyuan_2_1_image_to_3d_handler(
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
mysql_executor: &mut *mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+57
-29
@@ -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,17 +173,50 @@ 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: 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: &server_state.mysql_pool,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
@@ -208,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,
|
||||
|
||||
+49
-30
@@ -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,17 +173,50 @@ 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: 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: &server_state.mysql_pool,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
|
||||
+57
-30
@@ -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,17 +173,50 @@ 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: 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: &server_state.mysql_pool,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
@@ -209,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,
|
||||
|
||||
+57
-7
@@ -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,17 +175,50 @@ 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: 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: &server_state.mysql_pool,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
@@ -187,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,
|
||||
|
||||
+1
-5
@@ -53,11 +53,7 @@ pub async fn generate_veo_2_image_to_video_handler(
|
||||
) -> Result<Json<GenerateVeo2ImageToVideoResponse>, CommonWebError> {
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("MySql pool error: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
.await?;
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
|
||||
+91
-10
@@ -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,
|
||||
|
||||
+49
-5
@@ -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)
|
||||
|
||||
+51
-2
@@ -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"))?;
|
||||
@@ -117,4 +166,4 @@ pub async fn upload_image(
|
||||
info!("Image media file uploaded with token: {}", media_token);
|
||||
|
||||
Ok(media_token)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::http_server::endpoints::generate::image::edit::gpt_image_1_edit_image_handler::gpt_image_1_edit_image_handler;
|
||||
use crate::http_server::endpoints::generate::image::generate_flux_1_dev_text_to_image_handler::generate_flux_1_dev_text_to_image_handler;
|
||||
use crate::http_server::endpoints::generate::image::generate_flux_1_schnell_text_to_image_handler::generate_flux_1_schnell_text_to_image_handler;
|
||||
use crate::http_server::endpoints::generate::image::generate_flux_pro_11_text_to_image_handler::generate_flux_pro_11_text_to_image_handler;
|
||||
use crate::http_server::endpoints::generate::image::generate_flux_pro_11_ultra_text_to_image_handler::generate_flux_pro_11_ultra_text_to_image_handler;
|
||||
use crate::http_server::endpoints::generate::image::generate_gpt_image_1_text_to_image_handler::generate_gpt_image_1_text_to_image_handler;
|
||||
use crate::http_server::endpoints::generate::image::remove_image_background_handler::remove_image_background_handler;
|
||||
use crate::http_server::endpoints::generate::object::generate_hunyuan_2_0_image_to_3d_handler::generate_hunyuan_2_0_image_to_3d_handler;
|
||||
use crate::http_server::endpoints::generate::object::generate_hunyuan_2_1_image_to_3d_handler::generate_hunyuan_2_1_image_to_3d_handler;
|
||||
@@ -14,7 +16,6 @@ use actix_http::body::MessageBody;
|
||||
use actix_service::ServiceFactory;
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::{web, App, Error, HttpResponse};
|
||||
use crate::http_server::endpoints::generate::image::edit::gpt_image_1_edit_image_handler::gpt_image_1_edit_image_handler;
|
||||
|
||||
pub fn add_generate_routes<T, B> (app: App<T>) -> App<T>
|
||||
where
|
||||
@@ -51,6 +52,10 @@ where
|
||||
.route(web::post().to(generate_flux_pro_11_ultra_text_to_image_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::resource("/gpt_image_1_text_to_image")
|
||||
.route(web::post().to(generate_gpt_image_1_text_to_image_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::resource("/remove_background")
|
||||
.route(web::post().to(remove_image_background_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
|
||||
@@ -18,6 +18,22 @@ use crate::prelude::{Deserialize, FalRequest, Serialize};
|
||||
}
|
||||
*/
|
||||
|
||||
/// This is the non-edit, text-to-image only endpoint
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptTextToImageRequest {
|
||||
/// The prompt
|
||||
pub prompt: String,
|
||||
|
||||
pub image_size: String,
|
||||
|
||||
pub num_images: u8,
|
||||
|
||||
pub quality: String,
|
||||
|
||||
pub openai_api_key: String,
|
||||
}
|
||||
|
||||
/// This is edit-only, image-to-image endpoint
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptEditImageRequest {
|
||||
/// The context images
|
||||
@@ -39,6 +55,10 @@ pub struct GptEditImageRequest {
|
||||
pub struct ImageOutput {
|
||||
}
|
||||
|
||||
pub fn gpt_text_to_image(params: GptTextToImageRequest) -> FalRequest<GptTextToImageRequest, ImageOutput> {
|
||||
FalRequest::new("fal-ai/gpt-image-1/text-to-image/byok", params)
|
||||
}
|
||||
|
||||
pub fn gpt_edit_image(params: GptEditImageRequest) -> FalRequest<GptEditImageRequest, ImageOutput> {
|
||||
FalRequest::new("fal-ai/gpt-image-1/edit-image/byok", params)
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ import ModelBadgeGrid from "../../components/model-badge-grid";
|
||||
|
||||
//const MAC_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/mac/ArtCraft_0.0.1_universal_2025.06.04.dmg";
|
||||
//const MAC_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/mac/ArtCraft_0.0.1_universal_2025.07.13.dmg";
|
||||
const MAC_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/mac/ArtCraft_0.0.1_universal_2025.07.17.dmg";
|
||||
//const MAC_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/mac/ArtCraft_0.0.1_universal_2025.07.17.dmg";
|
||||
const MAC_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/mac/ArtCraft_0.0.1_universal_2025.07.28.dmg";
|
||||
|
||||
//const WINDOWS_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/windows/ArtCraft_0.0.1_x64-setup_2025.06.04.exe";
|
||||
//const WINDOWS_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/windows/ArtCraft_0.0.1_x64-setup_2025.07.13.exe";
|
||||
const WINDOWS_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/windows/ArtCraft_0.0.1_x64-setup_2025.07.17.exe";
|
||||
//const WINDOWS_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/windows/ArtCraft_0.0.1_x64-setup_2025.07.17.exe";
|
||||
const WINDOWS_LINK = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev/windows/ArtCraft_0.0.1_x64-setup_2025.07.28.exe";
|
||||
|
||||
const fadeUpVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Recraft</title><path d="M19.667 8.275c0-4.57-4.15-8.275-9.27-8.275-1.774 0-3.213 3.705-3.213 8.275 0 1.143.09 2.233.253 3.224H4.29L1 23h9.4v-6.447c5.117 0 9.266-3.707 9.266-8.275l.001-.002zm-9.27-6.76c.93 0 1.682 3.028 1.682 6.76 0 3.733-.752 6.76-1.681 6.76-.93 0-1.681-3.027-1.681-6.76 0-3.732.752-6.76 1.68-6.76z"></path><path d="M19.848 16.552h-9.44L14.028 23h9.438l-3.618-6.448z"></path></svg>
|
||||
|
After Width: | Height: | Size: 562 B |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Tencent</title><path d="M9.976 1L24 9.8l-10.587.015L10.723 23H5.489L8.18 9.8H3.244L1 5.4h8.077L9.976 1z"></path></svg>
|
||||
|
After Width: | Height: | Size: 282 B |
@@ -170,8 +170,15 @@ const PageDraw = () => {
|
||||
};
|
||||
|
||||
const onEnqueuedPressed = async () => {
|
||||
const { width, height } = store.getAspectRatioDimensions();
|
||||
|
||||
// takes snap shot and then a global variable in the engine will invoke the inference.
|
||||
const image = await captureStageImageBitmap(stageRef, transformerRefs);
|
||||
const image = await captureStageImageBitmap(
|
||||
stageRef,
|
||||
transformerRefs,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
if (!image) {
|
||||
console.error("Failed to capture stage image");
|
||||
return;
|
||||
|
||||
@@ -1,114 +1,129 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
import Konva from "konva";
|
||||
|
||||
const debugOutput = (uri:string, name:string)=> {
|
||||
var link = document.createElement('a');
|
||||
link.download = name;
|
||||
link.href = uri;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
export const captureStageImageBitmap = async (
|
||||
stageRef: React.RefObject<Konva.Stage>,
|
||||
transformerRefs: React.RefObject<{ [key: string]: Konva.Transformer }>,
|
||||
width: number = 1024,
|
||||
height: number = 1024,
|
||||
): Promise<ImageBitmap | undefined> => {
|
||||
if (!stageRef.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const oldPos = { ...stageRef.current.position() };
|
||||
const oldScale = {
|
||||
x: stageRef.current.scaleX(),
|
||||
y: stageRef.current.scaleY(),
|
||||
};
|
||||
|
||||
export const captureStageImageBitmap = async (stageRef: any,transformerRefs: any): Promise<ImageBitmap | undefined> => {
|
||||
if (!stageRef.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const oldPos = { ...stageRef.current.position() };
|
||||
const oldScale = { x: stageRef.current.scaleX(), y: stageRef.current.scaleY() };
|
||||
|
||||
// Hide all transformers
|
||||
Object.values(transformerRefs.current).forEach(transformer => {
|
||||
// Hide all transformers
|
||||
Object.values(transformerRefs.current || {}).forEach(
|
||||
(transformer: Konva.Transformer) => {
|
||||
transformer.visible(false);
|
||||
},
|
||||
);
|
||||
|
||||
stageRef.current.position({ x: 0, y: 0 });
|
||||
stageRef.current.scale({ x: 1, y: 1 });
|
||||
stageRef.current.draw();
|
||||
|
||||
try {
|
||||
const canvas = stageRef.current.toCanvas({
|
||||
width,
|
||||
height,
|
||||
pixelRatio: 1,
|
||||
});
|
||||
|
||||
|
||||
return await createImageBitmap(canvas);
|
||||
} catch (error) {
|
||||
console.error("Failed to create ImageBitmap:", error);
|
||||
return undefined;
|
||||
} finally {
|
||||
// Restore transformers
|
||||
Object.values(transformerRefs.current || {}).forEach(
|
||||
(transformer: Konva.Transformer) => {
|
||||
transformer.visible(true);
|
||||
},
|
||||
);
|
||||
|
||||
stageRef.current.position(oldPos);
|
||||
stageRef.current.scale(oldScale);
|
||||
stageRef.current.draw();
|
||||
}
|
||||
};
|
||||
|
||||
export const useStageSnapshot = (
|
||||
stageRef: React.RefObject<Konva.Stage>,
|
||||
imageRef: React.RefObject<Konva.Image>,
|
||||
isSelectingRef: React.RefObject<boolean>,
|
||||
transformerRefs: React.RefObject<{ [key: string]: Konva.Transformer }>,
|
||||
) => {
|
||||
const [animationFrameId, setAnimationFrameId] = useState<number | null>(null);
|
||||
|
||||
const captureStageImage = (
|
||||
stageRef: React.RefObject<Konva.Stage>,
|
||||
width: number = 1024,
|
||||
height: number = 1024,
|
||||
): string => {
|
||||
if (!stageRef.current) return "";
|
||||
|
||||
const oldPos = { ...stageRef.current.position() };
|
||||
const oldScale = {
|
||||
x: stageRef.current.scaleX(),
|
||||
y: stageRef.current.scaleY(),
|
||||
};
|
||||
|
||||
// Hide all transformers
|
||||
Object.values(transformerRefs.current || {}).forEach(
|
||||
(transformer: Konva.Transformer) => {
|
||||
transformer.visible(false);
|
||||
},
|
||||
);
|
||||
|
||||
stageRef.current.position({ x: 0, y: 0 });
|
||||
stageRef.current.scale({ x: 1, y: 1 });
|
||||
stageRef.current.draw();
|
||||
|
||||
try {
|
||||
const canvas = stageRef.current.toCanvas({
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
pixelRatio: 1
|
||||
});
|
||||
|
||||
return await createImageBitmap(canvas);
|
||||
} catch (error) {
|
||||
console.error('Failed to create ImageBitmap:', error);
|
||||
return undefined;
|
||||
} finally {
|
||||
// Restore transformers
|
||||
Object.values(transformerRefs.current).forEach(transformer => {
|
||||
const uri = stageRef.current.toDataURL({ width, height, pixelRatio: 1 });
|
||||
|
||||
// Restore transformers
|
||||
Object.values(transformerRefs.current || {}).forEach(
|
||||
(transformer: Konva.Transformer) => {
|
||||
transformer.visible(true);
|
||||
});
|
||||
|
||||
stageRef.current.position(oldPos);
|
||||
stageRef.current.scale(oldScale);
|
||||
stageRef.current.draw();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
stageRef.current.position(oldPos);
|
||||
stageRef.current.scale(oldScale);
|
||||
stageRef.current.draw();
|
||||
return uri;
|
||||
};
|
||||
|
||||
export const useStageSnapshot = (
|
||||
stageRef: React.RefObject<any>,
|
||||
imageRef: React.RefObject<any>,
|
||||
isSelectingRef: React.RefObject<boolean>,
|
||||
transformerRefs: React.RefObject<{ [key: string]: Konva.Transformer }>
|
||||
) => {
|
||||
const [animationFrameId, setAnimationFrameId] = useState<number | null>(null);
|
||||
|
||||
const captureStageImage = (stageRef: any): string => {
|
||||
const oldPos = { ...stageRef.current.position() };
|
||||
const oldScale = { x: stageRef.current.scaleX(), y: stageRef.current.scaleY() };
|
||||
|
||||
// Hide all transformers
|
||||
Object.values(transformerRefs.current).forEach(transformer => {
|
||||
transformer.visible(false);
|
||||
});
|
||||
|
||||
stageRef.current.position({ x: 0, y: 0 });
|
||||
stageRef.current.scale({ x: 1, y: 1 });
|
||||
stageRef.current.draw();
|
||||
const uri = stageRef.current.toDataURL({ width: 1024, height: 1024, pixelRatio: 1 });
|
||||
|
||||
// Restore transformers
|
||||
Object.values(transformerRefs.current).forEach(transformer => {
|
||||
transformer.visible(true);
|
||||
});
|
||||
|
||||
stageRef.current.position(oldPos);
|
||||
stageRef.current.scale(oldScale);
|
||||
stageRef.current.draw();
|
||||
return uri;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const updateSnapshot = () => {
|
||||
if (!isSelectingRef.current && stageRef.current && imageRef.current) {
|
||||
const uri = captureStageImage(stageRef);
|
||||
if (uri) {
|
||||
const img = new window.Image();
|
||||
img.onload = () => {
|
||||
imageRef.current?.setAttrs({
|
||||
image: img
|
||||
});
|
||||
};
|
||||
img.src = uri;
|
||||
}
|
||||
useEffect(() => {
|
||||
const updateSnapshot = () => {
|
||||
if (!isSelectingRef.current && stageRef.current && imageRef.current) {
|
||||
const uri = captureStageImage(stageRef);
|
||||
if (uri) {
|
||||
const img = new window.Image();
|
||||
img.onload = () => {
|
||||
imageRef.current?.setAttrs({
|
||||
image: img,
|
||||
});
|
||||
};
|
||||
img.src = uri;
|
||||
}
|
||||
const frameId = requestAnimationFrame(updateSnapshot);
|
||||
setAnimationFrameId(frameId);
|
||||
};
|
||||
|
||||
}
|
||||
const frameId = requestAnimationFrame(updateSnapshot);
|
||||
setAnimationFrameId(frameId);
|
||||
|
||||
return () => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
const frameId = requestAnimationFrame(updateSnapshot);
|
||||
setAnimationFrameId(frameId);
|
||||
|
||||
return () => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
+47
-18
@@ -152,8 +152,29 @@ export const ControlsTopButtons = () => {
|
||||
{
|
||||
label: "New scene",
|
||||
description: "Ctrl+N",
|
||||
onClick: () => {
|
||||
setSceneTitleInput("Untitled New Scene");
|
||||
dialogProps: {
|
||||
title: "Create New Scene",
|
||||
content: (
|
||||
<h4>
|
||||
Make sure you've saved your scene. Unsaved changes will
|
||||
be lost. Continue?
|
||||
</h4>
|
||||
),
|
||||
confirmButtonProps: {
|
||||
label: "Create new scene",
|
||||
onClick: async () => {
|
||||
handleResetScene();
|
||||
|
||||
const defaultTitle = "Untitled New Scene";
|
||||
setSceneTitleInput(defaultTitle);
|
||||
|
||||
await editorEngine?.newScene(defaultTitle);
|
||||
},
|
||||
},
|
||||
closeButtonProps: {
|
||||
label: "Cancel",
|
||||
},
|
||||
showClose: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -186,22 +207,30 @@ export const ControlsTopButtons = () => {
|
||||
// save scene should be disabled if there are no changes
|
||||
label: "Save scene",
|
||||
description: "Ctrl+S",
|
||||
dialogProps: {
|
||||
title: "Save Scene",
|
||||
content: (
|
||||
<h4>
|
||||
Save scene to <b>{scene.value.title}</b>?
|
||||
</h4>
|
||||
),
|
||||
confirmButtonProps: {
|
||||
label: "Save",
|
||||
onClick: handleButtonSave,
|
||||
},
|
||||
closeButtonProps: {
|
||||
label: "Cancel",
|
||||
},
|
||||
showClose: true,
|
||||
},
|
||||
...(scene.value.token
|
||||
? {
|
||||
// Existing scene → save directly without dialog
|
||||
onClick: handleButtonSave,
|
||||
}
|
||||
: {
|
||||
// New (unsaved) scene → ask for confirmation first
|
||||
dialogProps: {
|
||||
title: "Save Scene",
|
||||
content: (
|
||||
<h4>
|
||||
Save scene to <b>{scene.value.title}</b>?
|
||||
</h4>
|
||||
),
|
||||
confirmButtonProps: {
|
||||
label: "Save",
|
||||
onClick: handleButtonSave,
|
||||
},
|
||||
closeButtonProps: {
|
||||
label: "Cancel",
|
||||
},
|
||||
showClose: true,
|
||||
},
|
||||
}),
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,6 +18,9 @@ interface CompletedItem {
|
||||
public_bucket_path: string; // TODO: STOP USING THIS.
|
||||
maybe_media_links_thumbnail?: string;
|
||||
updated_at: string;
|
||||
maybe_result?: {
|
||||
entity_token?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// {
|
||||
@@ -144,17 +147,23 @@ export function Activity() {
|
||||
job.maybe_result?.maybe_public_bucket_media_path || "",
|
||||
maybe_media_links_thumbnail: ((job) => {
|
||||
// Try the video thumbnail first, then the regular thumbnail.
|
||||
const maybeVideoThumb = job.maybe_result?.media_links?.maybe_video_previews?.animated;
|
||||
const maybeGenericThumb = (job.maybe_result?.media_links?.maybe_thumbnail_template || "").replace("{WIDTH}", "500");
|
||||
const maybeVideoThumb =
|
||||
job.maybe_result?.media_links?.maybe_video_previews?.animated;
|
||||
const maybeGenericThumb = (
|
||||
job.maybe_result?.media_links?.maybe_thumbnail_template || ""
|
||||
).replace("{WIDTH}", "500");
|
||||
return maybeVideoThumb || maybeGenericThumb;
|
||||
})(job),
|
||||
updated_at: job.updated_at || new Date().toISOString(),
|
||||
maybe_result: job.maybe_result,
|
||||
}));
|
||||
|
||||
// Show toast only if not first load
|
||||
if (!isFirstLoad.current) {
|
||||
showToast(newItems);
|
||||
}
|
||||
// NB(bt,2025-07-28): Don't use eventing from here anymore.
|
||||
// Tauri is sending direct events we can bind toasts to.
|
||||
// // Show toast only if not first load
|
||||
// if (!isFirstLoad.current) {
|
||||
// showToast(newItems);
|
||||
// }
|
||||
|
||||
// Update state
|
||||
setCompletedItems((prev) => {
|
||||
@@ -217,39 +226,43 @@ export function Activity() {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="max-h-[480px] overflow-y-auto">
|
||||
{jobs.length > 0 && (
|
||||
<div>
|
||||
{jobs.map((job) => (
|
||||
<InProgressCard key={job.job_token} job={job} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isFirstLoad.current ? (
|
||||
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
spin
|
||||
size="2x"
|
||||
className="text-gray-400"
|
||||
/>
|
||||
<h3 className="text-gray-300">Retrieving Activities</h3>
|
||||
</div>
|
||||
) : jobs.length === 0 && completedItems.length === 0 ? (
|
||||
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
|
||||
<h3 className="text-lg text-gray-300">No activities yet</h3>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
{completedItems.map((item) => (
|
||||
<CompletedCard key={item.token} job={item} />
|
||||
{(close) => (
|
||||
<div className="max-h-[480px] overflow-y-auto">
|
||||
{jobs.length > 0 && (
|
||||
<div>
|
||||
{jobs.map((job) => (
|
||||
<InProgressCard key={job.job_token} job={job} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isFirstLoad.current ? (
|
||||
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
spin
|
||||
size="2x"
|
||||
className="text-gray-400"
|
||||
/>
|
||||
<h3 className="text-gray-300">Retrieving Activities</h3>
|
||||
</div>
|
||||
) : jobs.length === 0 && completedItems.length === 0 ? (
|
||||
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
|
||||
<h3 className="text-lg text-gray-300">No activities yet</h3>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
{completedItems.map((item) => (
|
||||
<div key={item.token} onClick={close}>
|
||||
<CompletedCard job={item} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PopoverMenu>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
+27
-27
@@ -1,7 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { LightboxModal } from "@storyteller/ui-lightbox-modal";
|
||||
import { GetCdnOrigin } from "~/api/GetCdnOrigin";
|
||||
import {
|
||||
galleryModalLightboxMediaId,
|
||||
galleryModalLightboxVisible,
|
||||
galleryModalLightboxImage,
|
||||
} from "@storyteller/ui-gallery-modal";
|
||||
import type { GalleryItem } from "@storyteller/ui-gallery-modal";
|
||||
|
||||
interface Props {
|
||||
job: {
|
||||
@@ -11,17 +15,18 @@ interface Props {
|
||||
public_bucket_path: string;
|
||||
maybe_style_name?: string;
|
||||
maybe_media_links_thumbnail?: string;
|
||||
maybe_result?: {
|
||||
entity_token?: string;
|
||||
media_links?: {
|
||||
cdn_url?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function CompletedCard({ job }: Props) {
|
||||
const [loadError, setLoadError] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const cdnOrigin = GetCdnOrigin();
|
||||
const mediaPath = job?.public_bucket_path || "";
|
||||
const downloadLink = `${cdnOrigin}${mediaPath}`;
|
||||
//const imageUrl = downloadLink;
|
||||
const fullImageUrl = downloadLink;
|
||||
// Info: Gallery modal will handle displaying full media. Compute thumbnail only.
|
||||
|
||||
// NB: Try to use the thumbnail instead of the link to the full asset.
|
||||
// Passing around full asset URLs and putting them into image tags is causing CORS errors if those assets are not images.
|
||||
@@ -33,7 +38,19 @@ export function CompletedCard({ job }: Props) {
|
||||
<>
|
||||
<div
|
||||
className="flex w-full items-center justify-between rounded-lg p-2 text-start transition-all duration-150 hover:cursor-pointer hover:bg-white/10"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
onClick={() => {
|
||||
galleryModalLightboxMediaId.value =
|
||||
job.maybe_result?.entity_token || null;
|
||||
galleryModalLightboxImage.value = {
|
||||
id: job.maybe_result?.entity_token || job.token,
|
||||
label: job.maybe_title || "Image",
|
||||
thumbnail: thumbnailUrl,
|
||||
fullImage: job.maybe_result?.media_links?.cdn_url || thumbnailUrl,
|
||||
createdAt: job.updated_at,
|
||||
mediaClass: undefined,
|
||||
} as GalleryItem;
|
||||
galleryModalLightboxVisible.value = true;
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex aspect-square h-14 w-14 justify-center overflow-hidden rounded-lg border border-[#A9A9A9]/50 bg-black/60">
|
||||
@@ -67,24 +84,7 @@ export function CompletedCard({ job }: Props) {
|
||||
</a> */}
|
||||
</div>
|
||||
|
||||
<LightboxModal
|
||||
onCloseGallery={() => setIsModalOpen(false)}
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
imageUrl={fullImageUrl}
|
||||
imageAlt={job.maybe_title ?? "unknown"}
|
||||
onImageError={() => setLoadError(true)}
|
||||
title={job.maybe_title}
|
||||
createdAt={job.updated_at}
|
||||
downloadUrl={downloadLink}
|
||||
additionalInfo={
|
||||
job.maybe_style_name && (
|
||||
<div className="text-sm text-white/60">
|
||||
Style: {job.maybe_style_name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{/* LightboxModal removed – now using top level GalleryModal */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager.js";
|
||||
import { ApiManager, ApiResponse } from "./ApiManager.js";
|
||||
import { Prompts } from "./models/Prompts.js";
|
||||
import { FetchProxy as fetch } from "@storyteller/tauri-utils";
|
||||
|
||||
export class PromptsApi extends ApiManager {
|
||||
|
||||
public async enqueueImageGeneration({
|
||||
disableSystemPrompt,
|
||||
prompt,
|
||||
@@ -48,7 +47,7 @@ export class PromptsApi extends ApiManager {
|
||||
|
||||
const result = {
|
||||
success: isSuccess,
|
||||
data: isSuccess ? postResponse.inference_job_token : undefined,
|
||||
data: isSuccess ? postResponse.job_token : undefined,
|
||||
errorMessage: isSuccess ? undefined : postResponse.BadInput,
|
||||
};
|
||||
|
||||
@@ -57,7 +56,7 @@ export class PromptsApi extends ApiManager {
|
||||
|
||||
public async pollJobSession(
|
||||
jobToken: string,
|
||||
thumbnailWidth: number = 256,
|
||||
thumbnailWidth = 256
|
||||
): Promise<
|
||||
ApiResponse<{
|
||||
job_token: string;
|
||||
@@ -92,7 +91,7 @@ export class PromptsApi extends ApiManager {
|
||||
const thumbnail_url =
|
||||
response.state.maybe_result?.media_links?.maybe_thumbnail_template?.replace(
|
||||
"{WIDTH}",
|
||||
thumbnailWidth.toString(),
|
||||
thumbnailWidth.toString()
|
||||
);
|
||||
|
||||
const progress_percentage = response.state.status.progress_percentage;
|
||||
|
||||
@@ -13,4 +13,22 @@ export interface Prompts {
|
||||
use_cinematic: boolean;
|
||||
used_face_detailer: boolean;
|
||||
used_upscaler: boolean;
|
||||
maybe_model_type: string;
|
||||
maybe_generation_provider: string;
|
||||
maybe_context_images: [
|
||||
{
|
||||
media_links: {
|
||||
cdn_url: string;
|
||||
maybe_thumbnail_template: string;
|
||||
maybe_video_previews: {
|
||||
animated: string;
|
||||
animated_thumbnail_template: string;
|
||||
still: string;
|
||||
still_thumbnail_template: string;
|
||||
};
|
||||
};
|
||||
media_token: string;
|
||||
semantic: string;
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ import {
|
||||
galleryModalVisibleDuringDrag,
|
||||
galleryReopenAfterDragSignal,
|
||||
galleryModalVisibleViewMode,
|
||||
galleryModalLightboxMediaId,
|
||||
galleryModalLightboxImage,
|
||||
galleryModalLightboxVisible,
|
||||
} from "./galleryModalSignals";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
@@ -85,10 +88,9 @@ export const GalleryModal = React.memo(
|
||||
forceFilter,
|
||||
}: GalleryModalProps) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [lightboxImage, setLightboxImage] = useState<GalleryItem | null>(
|
||||
null
|
||||
);
|
||||
const [isLightboxVisible, setIsLightboxVisible] = useState(false);
|
||||
// Lightbox state is now handled via signals
|
||||
const lightboxImageSignal = galleryModalLightboxImage;
|
||||
const lightboxVisibleSignal = galleryModalLightboxVisible;
|
||||
const [failedImageUrls] = useState<Set<string>>(new Set());
|
||||
const [username, setUsername] = useState<string>("");
|
||||
const forceFilterRef = useRef(forceFilter);
|
||||
@@ -114,7 +116,7 @@ export const GalleryModal = React.memo(
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const pageSize = 25;
|
||||
|
||||
const imageUrl = lightboxImage?.fullImage || "";
|
||||
const imageUrl = lightboxImageSignal.value?.fullImage || "";
|
||||
|
||||
const api = useMemo(() => new GalleryModalApi(), []);
|
||||
const usersApi = useMemo(() => new UsersApi(), []);
|
||||
@@ -296,17 +298,18 @@ export const GalleryModal = React.memo(
|
||||
if (mode === "select" && onSelectItem) {
|
||||
onSelectItem(item.id);
|
||||
} else {
|
||||
setLightboxImage(item);
|
||||
setIsLightboxVisible(true);
|
||||
lightboxImageSignal.value = item;
|
||||
lightboxVisibleSignal.value = true;
|
||||
galleryModalLightboxMediaId.value = item.id;
|
||||
}
|
||||
},
|
||||
[mode, onSelectItem]
|
||||
);
|
||||
|
||||
const handleCloseLightbox = useCallback(() => {
|
||||
setIsLightboxVisible(false);
|
||||
// Wait for animation to complete before removing the image
|
||||
setTimeout(() => setLightboxImage(null), 300);
|
||||
lightboxVisibleSignal.value = false;
|
||||
lightboxImageSignal.value = null;
|
||||
galleryModalLightboxMediaId.value = null;
|
||||
}, []);
|
||||
|
||||
const handleDeselectAll = useCallback(() => {
|
||||
@@ -322,6 +325,18 @@ export const GalleryModal = React.memo(
|
||||
|
||||
useSignals();
|
||||
|
||||
// Synchronize lightbox image when external media id signal is set
|
||||
useEffect(() => {
|
||||
if (galleryModalLightboxMediaId.value && allItems.length > 0) {
|
||||
const target = allItems.find(
|
||||
(it) => it.id === galleryModalLightboxMediaId.value
|
||||
);
|
||||
if (target) {
|
||||
lightboxImageSignal.value = target;
|
||||
}
|
||||
}
|
||||
}, [galleryModalLightboxMediaId.value, allItems]);
|
||||
|
||||
// Compute gap class based on gridColumns
|
||||
let gapClass = "gap-0.5";
|
||||
if (gridColumns <= 4) gapClass = "gap-1.5";
|
||||
@@ -625,25 +640,23 @@ export const GalleryModal = React.memo(
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<LightboxModal
|
||||
isOpen={isLightboxVisible}
|
||||
onClose={handleCloseLightbox}
|
||||
onCloseGallery={() =>
|
||||
mode === "view"
|
||||
? (galleryModalVisibleViewMode.value = false)
|
||||
: onClose?.()
|
||||
}
|
||||
imageUrl={imageUrl}
|
||||
imageAlt={lightboxImage?.label || ""}
|
||||
onImageError={() => imageUrl && handleImageError(imageUrl)}
|
||||
title={lightboxImage?.label}
|
||||
createdAt={lightboxImage?.createdAt}
|
||||
downloadUrl={imageUrl}
|
||||
mediaId={lightboxImage?.id}
|
||||
onDownloadClicked={onDownloadClicked}
|
||||
onAddToSceneClicked={onAddToSceneClicked}
|
||||
mediaClass={lightboxImage?.mediaClass}
|
||||
/>
|
||||
{mode === "view" && (
|
||||
<LightboxModal
|
||||
isOpen={lightboxVisibleSignal.value}
|
||||
onClose={handleCloseLightbox}
|
||||
onCloseGallery={() => (galleryModalVisibleViewMode.value = false)}
|
||||
imageUrl={imageUrl}
|
||||
imageAlt={lightboxImageSignal.value?.label || ""}
|
||||
onImageError={() => imageUrl && handleImageError(imageUrl)}
|
||||
title={lightboxImageSignal.value?.label}
|
||||
createdAt={lightboxImageSignal.value?.createdAt}
|
||||
downloadUrl={imageUrl}
|
||||
mediaId={lightboxImageSignal.value?.id}
|
||||
onDownloadClicked={onDownloadClicked}
|
||||
onAddToSceneClicked={onAddToSceneClicked}
|
||||
mediaClass={lightboxImageSignal.value?.mediaClass}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { signal } from "@preact/signals-react";
|
||||
|
||||
// Gallery Modal Signals
|
||||
export const galleryModalVisibleDuringDrag = signal(true);
|
||||
export const galleryReopenAfterDragSignal = signal(false);
|
||||
export const galleryModalVisibleViewMode = signal(false);
|
||||
|
||||
// Lightbox Modal Signals
|
||||
export const galleryModalLightboxMediaId = signal<string | null>(null);
|
||||
export const galleryModalLightboxVisible = signal(false);
|
||||
export const galleryModalLightboxImage = signal<any>(null);
|
||||
|
||||
@@ -9,6 +9,17 @@ import {
|
||||
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
||||
import React from "react";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
import { MediaFilesApi, PromptsApi } from "@storyteller/api";
|
||||
import { toast } from "@storyteller/ui-toaster";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCopy } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import {
|
||||
getModelCreatorIcon,
|
||||
getModelDisplayName,
|
||||
getProviderDisplayName,
|
||||
} from "@storyteller/model-list";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
|
||||
interface LightboxModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -28,6 +39,7 @@ interface LightboxModalProps {
|
||||
media_id: string | undefined
|
||||
) => Promise<void>;
|
||||
mediaClass?: string;
|
||||
onPromptCopy?: (prompt: string) => void;
|
||||
}
|
||||
|
||||
export function LightboxModal({
|
||||
@@ -53,23 +65,100 @@ export function LightboxModal({
|
||||
const imageTagImageUrl = imageUrl ? imageUrl + "?cors=1" : "";
|
||||
|
||||
const [mediaLoaded, setMediaLoaded] = React.useState<boolean>(false);
|
||||
const [prompt, setPrompt] = React.useState<string | null>(null);
|
||||
const [promptLoading, setPromptLoading] = React.useState<boolean>(false);
|
||||
const [hasPromptToken, setHasPromptToken] = React.useState<boolean>(false);
|
||||
const [isPromptHovered, setIsPromptHovered] = React.useState<boolean>(false);
|
||||
const [generationProvider, setGenerationProvider] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [modelType, setModelType] = React.useState<string | null>(null);
|
||||
const [contextImages, setContextImages] = React.useState<Array<{
|
||||
media_links: {
|
||||
cdn_url: string;
|
||||
maybe_thumbnail_template: string;
|
||||
};
|
||||
media_token: string;
|
||||
semantic: string;
|
||||
}> | null>(null);
|
||||
|
||||
// Reset when imageUrl changes
|
||||
React.useEffect(() => {
|
||||
setMediaLoaded(false);
|
||||
}, [imageUrl]);
|
||||
|
||||
// Fetch prompt when mediaId changes
|
||||
React.useEffect(() => {
|
||||
const fetchPrompt = async () => {
|
||||
if (!mediaId) {
|
||||
setPrompt(null);
|
||||
setHasPromptToken(false);
|
||||
setGenerationProvider(null);
|
||||
setModelType(null);
|
||||
setContextImages(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setPromptLoading(true);
|
||||
try {
|
||||
const mediaFilesApi = new MediaFilesApi();
|
||||
const mediaResponse = await mediaFilesApi.GetMediaFileByToken({
|
||||
mediaFileToken: mediaId,
|
||||
});
|
||||
|
||||
if (mediaResponse.success && mediaResponse.data?.maybe_prompt_token) {
|
||||
setHasPromptToken(true);
|
||||
const promptsApi = new PromptsApi();
|
||||
const promptResponse = await promptsApi.GetPromptsByToken({
|
||||
token: mediaResponse.data.maybe_prompt_token,
|
||||
});
|
||||
|
||||
if (promptResponse.success && promptResponse.data) {
|
||||
const promptData = promptResponse.data;
|
||||
setPrompt(promptData.maybe_positive_prompt || null);
|
||||
setGenerationProvider(promptData.maybe_generation_provider || null);
|
||||
setModelType(promptData.maybe_model_type || null);
|
||||
setContextImages(promptData.maybe_context_images || null);
|
||||
} else {
|
||||
setPrompt(null);
|
||||
setGenerationProvider(null);
|
||||
setModelType(null);
|
||||
setContextImages(null);
|
||||
}
|
||||
} else {
|
||||
setHasPromptToken(false);
|
||||
setPrompt(null);
|
||||
setGenerationProvider(null);
|
||||
setModelType(null);
|
||||
setContextImages(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching prompt:", error);
|
||||
setHasPromptToken(false);
|
||||
setPrompt(null);
|
||||
setGenerationProvider(null);
|
||||
setModelType(null);
|
||||
setContextImages(null);
|
||||
} finally {
|
||||
setPromptLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPrompt();
|
||||
}, [mediaId]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
className="rounded-xl bg-[#2C2C2C] h-[80vh] w-[60vw] max-w-screen min-w-[35vw] min-h-[40vh]"
|
||||
className="rounded-xl bg-[#2C2C2C] h-[75vh] w-[60vw] max-w-screen min-w-[35vw] min-h-[40vh] p-4"
|
||||
draggable
|
||||
allowBackgroundInteraction={true}
|
||||
showClose={true}
|
||||
closeOnOutsideClick={false}
|
||||
resizable={true}
|
||||
backdropClassName="pointer-events-none hidden"
|
||||
expandable={true}
|
||||
>
|
||||
{/* Invisible drag handle strip at the very top for moving */}
|
||||
<Modal.DragHandle>
|
||||
@@ -115,15 +204,178 @@ export function LightboxModal({
|
||||
|
||||
{/* info + actions */}
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="text-xl font-medium">
|
||||
<div className="flex-1 space-y-5">
|
||||
{/* <div className="text-xl font-medium">
|
||||
{title || "Image Generation"}
|
||||
</div>
|
||||
</div> */}
|
||||
{createdAt && (
|
||||
<div className="text-sm text-white/60">
|
||||
Created: {dayjs(createdAt).format("MMM D, YYYY HH:mm:ss")}
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-sm font-medium text-white/90">Created</div>
|
||||
<div className="text-sm text-white/70">
|
||||
{dayjs(createdAt).format("MMM D, YYYY")} at{" "}
|
||||
{dayjs(createdAt).format("hh:mm A")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasPromptToken && (
|
||||
<>
|
||||
{/* Prompt */}
|
||||
<div className="relative space-y-1.5">
|
||||
<div className="text-sm font-medium text-white/90">
|
||||
Prompt
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"relative text-sm text-white/90 break-words bg-black/20 p-3 rounded-lg cursor-pointer transition-colors duration-100 leading-relaxed",
|
||||
isPromptHovered && "bg-black/30"
|
||||
)}
|
||||
onMouseEnter={() => setIsPromptHovered(true)}
|
||||
onMouseLeave={() => setIsPromptHovered(false)}
|
||||
onClick={() => {
|
||||
if (!prompt) return;
|
||||
navigator.clipboard.writeText(prompt).catch(() => {});
|
||||
toast.success("Prompt copied");
|
||||
}}
|
||||
>
|
||||
{promptLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<LoadingSpinner className="h-4 w-4" />
|
||||
<span className="text-sm text-white/80">
|
||||
Loading prompt...
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
prompt || (
|
||||
<span className="text-sm text-white/90">No prompt</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!promptLoading && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"pointer-events-none absolute inset-0 flex items-end justify-end opacity-0 transition-opacity duration-50",
|
||||
isPromptHovered && "opacity-100"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1 text-xs text-white/80 bg-black/80 backdrop-blur-md p-1.5 rounded-tl-lg rounded-br-lg">
|
||||
<FontAwesomeIcon icon={faCopy} className="h-3 w-3" />
|
||||
<span>Copy prompt</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Context Images */}
|
||||
{contextImages && contextImages.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-sm font-medium text-white/90">
|
||||
Reference Images
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-2">
|
||||
{contextImages.map((contextImage, index) => {
|
||||
const thumbnailUrl = contextImage.media_links
|
||||
.maybe_thumbnail_template
|
||||
? contextImage.media_links.maybe_thumbnail_template.replace(
|
||||
"{WIDTH}",
|
||||
"128"
|
||||
)
|
||||
: contextImage.media_links.cdn_url;
|
||||
|
||||
const fullSizeUrl = contextImage.media_links
|
||||
.maybe_thumbnail_template
|
||||
? contextImage.media_links.maybe_thumbnail_template.replace(
|
||||
"{WIDTH}",
|
||||
"512"
|
||||
)
|
||||
: contextImage.media_links.cdn_url;
|
||||
|
||||
return (
|
||||
// Hover preview
|
||||
<Tooltip
|
||||
key={contextImage.media_token}
|
||||
className="bg-black p-1.5"
|
||||
content={
|
||||
<div>
|
||||
<div className="flex flex-col items-center bg-white/10 rounded-lg">
|
||||
<img
|
||||
src={fullSizeUrl}
|
||||
alt={`Reference image ${index + 1} preview`}
|
||||
className="w-auto h-48 object-cover rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
{contextImage.semantic && (
|
||||
<div className="mt-2 text-xs text-white/90 text-center max-w-48 px-1">
|
||||
{contextImage.semantic}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
position="top"
|
||||
delay={300}
|
||||
closeOnClick={true}
|
||||
>
|
||||
<div
|
||||
className="relative group cursor-pointer"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
contextImage.media_links.cdn_url,
|
||||
"_blank"
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="relative overflow-hidden rounded-lg border border-white/5 bg-white/10 aspect-square">
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
alt={`Reference image ${index + 1}`}
|
||||
className="w-full h-full object-cover transition-transform duration-200 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generation Details */}
|
||||
{(generationProvider || modelType) && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-sm font-medium text-white/90">
|
||||
Generation Details
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{modelType && (
|
||||
<div className="flex items-center justify-between py-2 px-3 bg-black/20 rounded-lg border border-white/5">
|
||||
<span className="text-sm text-white/70 font-medium">
|
||||
Model
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{getModelCreatorIcon(modelType)}
|
||||
<span className="text-sm text-white/90 rounded">
|
||||
{getModelDisplayName(modelType)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{generationProvider && (
|
||||
<div className="flex items-center justify-between py-2 px-3 bg-black/20 rounded-lg border border-white/5">
|
||||
<span className="text-sm text-white/70 font-medium">
|
||||
Provider
|
||||
</span>
|
||||
<span className="text-sm text-white/90 rounded">
|
||||
{getProviderDisplayName(generationProvider)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{additionalInfo}
|
||||
</div>
|
||||
|
||||
@@ -164,7 +416,7 @@ export function LightboxModal({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{downloadUrl &&
|
||||
{/* {downloadUrl &&
|
||||
(onDownloadClicked ? (
|
||||
<Button
|
||||
icon={faDownToLine}
|
||||
@@ -188,7 +440,20 @@ export function LightboxModal({
|
||||
>
|
||||
<Button icon={faDownToLine}>Download</Button>
|
||||
</a>
|
||||
))}
|
||||
))} */}
|
||||
|
||||
{onDownloadClicked && downloadUrl && (
|
||||
<Button
|
||||
icon={faDownToLine}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("download_clicked");
|
||||
await onDownloadClicked(downloadUrl, mediaClass);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -949,9 +949,10 @@ export const Modal = ({
|
||||
{/* resize handles inside panel so clicks don't count as outside */}
|
||||
{renderResizeHandles()}
|
||||
</DialogPanel>
|
||||
{showClose && (
|
||||
<div className="absolute top-0 right-0 m-2.5 z-[80]">
|
||||
<CloseButton onClick={onClose} />
|
||||
{(showClose || expandable) && (
|
||||
<div className="absolute top-0 right-0 m-2.5 z-[80] flex items-center gap-2">
|
||||
{expandable && <Modal.ExpandButton />}
|
||||
{showClose && <CloseButton onClick={onClose} />}
|
||||
</div>
|
||||
)}
|
||||
</TransitionChild>
|
||||
|
||||
@@ -40,7 +40,7 @@ interface PopoverMenuProps {
|
||||
showIconsInList?: boolean;
|
||||
mode?: "default" | "toggle" | "button" | "hoverSelect";
|
||||
triggerLabel?: string | ReactNode;
|
||||
children?: ReactNode;
|
||||
children?: ReactNode | ((close: () => void) => ReactNode);
|
||||
buttonClassName?: string;
|
||||
panelClassName?: string;
|
||||
onPanelAction?: (action: string) => void;
|
||||
@@ -269,7 +269,11 @@ export const PopoverMenu = ({
|
||||
</div>
|
||||
)}
|
||||
{mode === "default" && children ? (
|
||||
<div className="text-sm text-white">{children}</div>
|
||||
<div className="text-sm text-white">
|
||||
{typeof children === "function"
|
||||
? children(close)
|
||||
: children}
|
||||
</div>
|
||||
) : mode === "hoverSelect" ? (
|
||||
<div className="flex flex-col gap-0 text-sm text-white">
|
||||
{items.map((item, index) => (
|
||||
@@ -365,7 +369,7 @@ export const PopoverMenu = ({
|
||||
<div key={index}>
|
||||
<Button
|
||||
className={twMerge(
|
||||
"flex w-full items-center justify-between border-transparent bg-transparent px-1.5",
|
||||
"flex w-full items-center shadow-none justify-between border-transparent bg-transparent px-1.5",
|
||||
"hover:bg-[#63636B]/60",
|
||||
mode === "toggle" && item.selected
|
||||
? "hover:bg-[#63636B]"
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
faTimes,
|
||||
faPlus,
|
||||
faImages,
|
||||
faCopy,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faRectangle,
|
||||
@@ -72,6 +73,8 @@ export const PromptBoxImage = ({
|
||||
const setUseSystemPrompt = usePromptImageStore((s) => s.setUseSystemPrompt);
|
||||
const aspectRatio = usePromptImageStore((s) => s.aspectRatio);
|
||||
const setAspectRatio = usePromptImageStore((s) => s.setAspectRatio);
|
||||
const generationCount = usePromptImageStore((s) => s.generationCount);
|
||||
const setGenerationCount = usePromptImageStore((s) => s.setGenerationCount);
|
||||
const [isEnqueueing, setIsEnqueueing] = useState(false);
|
||||
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
|
||||
[]
|
||||
@@ -98,6 +101,30 @@ export const PromptBoxImage = ({
|
||||
icon: <FontAwesomeIcon icon={faSquare} className="h-4 w-4" />,
|
||||
},
|
||||
]);
|
||||
const [generationCountList, setGenerationCountList] = useState<PopoverItem[]>(
|
||||
[
|
||||
{
|
||||
label: "1",
|
||||
selected: generationCount === 1,
|
||||
icon: <FontAwesomeIcon icon={faCopy} className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: "2",
|
||||
selected: generationCount === 2,
|
||||
icon: <FontAwesomeIcon icon={faCopy} className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: "3",
|
||||
selected: generationCount === 3,
|
||||
icon: <FontAwesomeIcon icon={faCopy} className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: "4",
|
||||
selected: generationCount === 4,
|
||||
icon: <FontAwesomeIcon icon={faCopy} className="h-4 w-4" />,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -121,6 +148,24 @@ export const PromptBoxImage = ({
|
||||
}
|
||||
}, [imageMediaId, url]);
|
||||
|
||||
useEffect(() => {
|
||||
setAspectRatioList((prev) =>
|
||||
prev.map((item) => ({
|
||||
...item,
|
||||
selected: item.label === aspectRatio,
|
||||
}))
|
||||
);
|
||||
}, [aspectRatio]);
|
||||
|
||||
useEffect(() => {
|
||||
setGenerationCountList((prev) =>
|
||||
prev.map((item) => ({
|
||||
...item,
|
||||
selected: item.label === generationCount.toString(),
|
||||
}))
|
||||
);
|
||||
}, [generationCount]);
|
||||
|
||||
const handleAspectRatioSelect = (selectedItem: PopoverItem) => {
|
||||
setAspectRatio(selectedItem.label as any);
|
||||
setAspectRatioList((prev) =>
|
||||
@@ -131,6 +176,17 @@ export const PromptBoxImage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const handleGenerationCountSelect = (selectedItem: PopoverItem) => {
|
||||
const count = parseInt(selectedItem.label, 10);
|
||||
setGenerationCount(count);
|
||||
setGenerationCountList((prev) =>
|
||||
prev.map((item) => ({
|
||||
...item,
|
||||
selected: item.label === selectedItem.label,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveReference = (id: string) => {
|
||||
setReferenceImages(referenceImages.filter((img) => img.id !== id));
|
||||
if (fileInputRef.current) {
|
||||
@@ -197,8 +253,6 @@ export const PromptBoxImage = ({
|
||||
const handleEnqueue = async () => {
|
||||
setIsEnqueueing(true);
|
||||
|
||||
console.log("PromptBoxImage - Prompting with model", modelInfo);
|
||||
|
||||
gtagEvent("enqueue_image");
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -214,6 +268,7 @@ export const PromptBoxImage = ({
|
||||
prompt: prompt,
|
||||
model: modelInfo,
|
||||
aspect_ratio: aspectRatio,
|
||||
number_images: generationCount,
|
||||
});
|
||||
|
||||
console.log("PromptBoxImage - generateResponse", generateResponse);
|
||||
@@ -382,6 +437,24 @@ export const PromptBoxImage = ({
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip
|
||||
content="Number of generations"
|
||||
position="top"
|
||||
className="z-50"
|
||||
closeOnClick={true}
|
||||
>
|
||||
<PopoverMenu
|
||||
items={generationCountList}
|
||||
onSelect={handleGenerationCountSelect}
|
||||
mode="toggle"
|
||||
triggerIcon={
|
||||
<FontAwesomeIcon icon={faCopy} className="h-4 w-4" />
|
||||
}
|
||||
panelClassName="min-w-28"
|
||||
panelTitle="No. of images"
|
||||
buttonClassName="h-9"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button
|
||||
className="flex items-center border-none bg-primary px-3 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||
icon={!isEnqueueing ? faSparkles : undefined}
|
||||
|
||||
@@ -59,10 +59,12 @@ interface PromptImageStore {
|
||||
aspectRatio: AspectRatio;
|
||||
useSystemPrompt: boolean;
|
||||
referenceImages: RefImage[];
|
||||
generationCount: number;
|
||||
setPrompt: (prompt: string) => void;
|
||||
setAspectRatio: (ratio: AspectRatio) => void;
|
||||
setUseSystemPrompt: (value: boolean) => void;
|
||||
setReferenceImages: (images: RefImage[]) => void;
|
||||
setGenerationCount: (count: number) => void;
|
||||
}
|
||||
|
||||
export const usePromptImageStore = create<PromptImageStore>()((set) => ({
|
||||
@@ -70,10 +72,12 @@ export const usePromptImageStore = create<PromptImageStore>()((set) => ({
|
||||
aspectRatio: "3:2",
|
||||
useSystemPrompt: true,
|
||||
referenceImages: [],
|
||||
generationCount: 1,
|
||||
setPrompt: (prompt) => set({ prompt }),
|
||||
setAspectRatio: (aspectRatio) => set({ aspectRatio }),
|
||||
setUseSystemPrompt: (useSystemPrompt) => set({ useSystemPrompt }),
|
||||
setReferenceImages: (referenceImages) => set({ referenceImages }),
|
||||
setGenerationCount: (generationCount) => set({ generationCount }),
|
||||
}));
|
||||
|
||||
// ----- Video Prompt Box Store -----
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from "./lib/ModelCreator.js";
|
||||
export * from "./lib/ImageModels.js";
|
||||
export * from "./lib/VideoModels.js";
|
||||
export * from "./lib/ModelCreatorIcons.js";
|
||||
export * from "./lib/ModelTypeMapping.js";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export enum ModelCreator {
|
||||
BlackForestLabs = "BlackForestLabs",
|
||||
Bytedance = "Bytedance",
|
||||
@@ -9,4 +8,6 @@ export enum ModelCreator {
|
||||
OpenAi = "OpenAi",
|
||||
Runway = "Runway",
|
||||
Stability = "Stability",
|
||||
Tencent = "Tencent",
|
||||
Recraft = "Recraft",
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ export const CREATOR_ICON_PATHS: Partial<Record<ModelCreator, string>> = {
|
||||
[ModelCreator.OpenAi]: "/resources/images/services/openai.svg",
|
||||
[ModelCreator.Bytedance]: "/resources/images/services/bytedance.svg",
|
||||
[ModelCreator.Google]: "/resources/images/services/google.svg",
|
||||
[ModelCreator.Recraft]: "/resources/images/services/recraft.svg",
|
||||
[ModelCreator.Tencent]: "/resources/images/services/tencent.svg",
|
||||
};
|
||||
|
||||
export const getCreatorIconPath = (creator: ModelCreator): string | undefined =>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from "react";
|
||||
import { ModelCreator } from "./ModelCreator.js";
|
||||
import { getCreatorIcon } from "./ModelCreatorIcons.js";
|
||||
|
||||
// Map from model type strings to ModelCreator enum values
|
||||
export const MODEL_TYPE_TO_CREATOR: Record<string, ModelCreator> = {
|
||||
flux_1_dev: ModelCreator.BlackForestLabs,
|
||||
flux_1_schnell: ModelCreator.BlackForestLabs,
|
||||
flux_pro_1p1: ModelCreator.BlackForestLabs,
|
||||
flux_pro_1p1_ultra: ModelCreator.BlackForestLabs,
|
||||
gpt_image_1: ModelCreator.OpenAi,
|
||||
kling_1p6_pro: ModelCreator.Kling,
|
||||
kling_2p1_pro: ModelCreator.Kling,
|
||||
kling_2p1_master: ModelCreator.Kling,
|
||||
seedance_1p0_lite: ModelCreator.Bytedance,
|
||||
veo_2: ModelCreator.Google,
|
||||
recraft_3: ModelCreator.Recraft,
|
||||
hunyuan_3d: ModelCreator.Tencent,
|
||||
};
|
||||
|
||||
// Get creator icon for a model type
|
||||
export const getModelCreatorIcon = (
|
||||
modelType: string
|
||||
): React.ReactNode | null => {
|
||||
const creator = MODEL_TYPE_TO_CREATOR[modelType];
|
||||
if (!creator) return null;
|
||||
return getCreatorIcon(creator, "h-4 w-4 invert");
|
||||
};
|
||||
|
||||
// Get creator name for display
|
||||
export const getModelCreatorName = (modelType: string): string | null => {
|
||||
const creator = MODEL_TYPE_TO_CREATOR[modelType];
|
||||
|
||||
// Convert enum value to display name
|
||||
switch (creator) {
|
||||
case ModelCreator.BlackForestLabs:
|
||||
return "Black Forest Labs";
|
||||
case ModelCreator.OpenAi:
|
||||
return "OpenAI";
|
||||
case ModelCreator.Kling:
|
||||
return "Kling AI";
|
||||
case ModelCreator.Bytedance:
|
||||
return "ByteDance";
|
||||
case ModelCreator.Google:
|
||||
return "Google";
|
||||
case ModelCreator.Midjourney:
|
||||
return "Midjourney";
|
||||
case ModelCreator.Stability:
|
||||
return "Stability AI";
|
||||
case ModelCreator.Runway:
|
||||
return "Runway";
|
||||
case ModelCreator.Hailuo:
|
||||
return "Hailuo";
|
||||
case ModelCreator.Recraft:
|
||||
return "Recraft";
|
||||
case ModelCreator.Tencent:
|
||||
return "Tencent";
|
||||
default:
|
||||
return creator;
|
||||
}
|
||||
};
|
||||
|
||||
// Convert model type string to human-readable display name
|
||||
export const getModelDisplayName = (modelType: string): string => {
|
||||
const displayNames: Record<string, string> = {
|
||||
flux_1_dev: "Flux 1 Dev",
|
||||
flux_1_schnell: "Flux 1 Schnell",
|
||||
flux_pro_1p1: "Flux Pro 1.1",
|
||||
flux_pro_1p1_ultra: "Flux Pro 1.1 Ultra",
|
||||
gpt_image_1: "GPT Image 1",
|
||||
kling_1p6_pro: "Kling 1.6 Pro",
|
||||
kling_2p1_pro: "Kling 2.1 Pro",
|
||||
kling_2p1_master: "Kling 2.1 Master",
|
||||
seedance_1p0_lite: "Seedance 1.0 Lite",
|
||||
veo_2: "Veo 2",
|
||||
recraft_3: "Recraft 3",
|
||||
hunyuan_3d_2p0: "Hunyuan 3D 2.0",
|
||||
hunyuan_3d_2p1: "Hunyuan 3D 2.1",
|
||||
hunyuan_3d: "Hunyuan 3D",
|
||||
};
|
||||
|
||||
return displayNames[modelType] || modelType;
|
||||
};
|
||||
|
||||
// Convert provider string to human-readable display name
|
||||
export const getProviderDisplayName = (provider: string): string => {
|
||||
const displayNames: Record<string, string> = {
|
||||
artcraft: "ArtCraft",
|
||||
fal: "FAL",
|
||||
sora: "Sora",
|
||||
};
|
||||
|
||||
return displayNames[provider] || provider;
|
||||
};
|
||||
@@ -17,12 +17,14 @@ export interface EnqueueTextToImageRequest {
|
||||
prompt?: string;
|
||||
model?: ModelInfo | EnqueueTextToImageModel;
|
||||
aspect_ratio?: EnqueueTextToImageSize;
|
||||
number_images?: number;
|
||||
}
|
||||
|
||||
interface EnqueueTextToImageRawRequest {
|
||||
prompt?: string;
|
||||
model?: EnqueueTextToImageModel | string; // TODO: Shouldn't allow string
|
||||
aspect_ratio?: EnqueueTextToImageSize;
|
||||
number_images?: number;
|
||||
}
|
||||
|
||||
export enum EnqueueTextToImageModel {
|
||||
@@ -75,6 +77,10 @@ export const EnqueueTextToImage = async (request: EnqueueTextToImageRequest) : P
|
||||
mutableRequest.aspect_ratio = request.aspect_ratio;
|
||||
}
|
||||
|
||||
if (!!request.number_images) {
|
||||
mutableRequest.number_images = request.number_images;
|
||||
}
|
||||
|
||||
const result = await invoke("enqueue_text_to_image_command", {
|
||||
request: mutableRequest,
|
||||
});
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
backup
|
||||
.venv
|
||||
# Editor directories and files
|
||||
~
|
||||
.vscode
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
**/*.onnx
|
||||
@@ -1,3 +0,0 @@
|
||||
@awesome.me:registry=https://npm.fontawesome.com/
|
||||
@fortawesome:registry=https://npm.fontawesome.com/
|
||||
//npm.fontawesome.com/:_authToken=104779DB-FD41-4BEA-BDB4-B1845D758A79
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"useTabs": false,
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Not Found</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1> This Page Was Not Found </h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,39 +0,0 @@
|
||||
# Memeboard
|
||||
|
||||
## Installation & Dev
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
yarn # this installs the node packages
|
||||
yarn dev # this runs the vite dev server
|
||||
```
|
||||
|
||||
You may need to turn off browser security to get around CORS restrictions:
|
||||
|
||||
### Windows 11
|
||||
|
||||
from your powershell
|
||||
|
||||
```sh
|
||||
# Chrome
|
||||
Start "C:\Program Files\Google\Chrome\Application\chrome.exe" -ArgumentList '--user-data-dir="C://chrome-dev-disabled-security" --disable-web-security --disable-site-isolation-trials'
|
||||
```
|
||||
|
||||
### on MAC
|
||||
|
||||
form your terminal
|
||||
|
||||
```sh
|
||||
# Chrome
|
||||
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --disable-web-security --user-data-dir=~/chrome
|
||||
```
|
||||
|
||||
### Inspecting the Shared Workers
|
||||
|
||||
on chrome, type `chrome://inspect/#workers` into the url field
|
||||
@@ -1,3 +0,0 @@
|
||||
/*
|
||||
Access-Control-Allow-Origin: *
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# Board Netlify Redirects File
|
||||
|
||||
# Netlify docs:
|
||||
# The redirects engine will process the first matching rule it
|
||||
# finds, reading from top to bottom. Rules in the _redirects file
|
||||
# are always processed first, followed by rules in the Netlify
|
||||
# configuration file.
|
||||
|
||||
/landing /index.html 200!
|
||||
/login /index.html 200!
|
||||
/m_* /index.html 200!
|
||||
@@ -1,44 +0,0 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
import react from "eslint-plugin-react";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
react,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs["jsx-runtime"].rules,
|
||||
// TODO WIL: make no-reaching rule for imports
|
||||
// "import/no-internal-modules": [
|
||||
// "error",
|
||||
// {
|
||||
// allow: [],
|
||||
// forbid: ["**/uiAccess/*"],
|
||||
// },
|
||||
// ],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1,24 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-ico" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Syne:wght@600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>ArtCraft Canvas</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/app/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,33 +0,0 @@
|
||||
[build]
|
||||
command = "./apps/editor2d/script/netlify_build.sh"
|
||||
publish = "apps/editor2d/dist/"
|
||||
|
||||
#[build.environment]
|
||||
#REACT_APP_PUBLIC_POSTHOG_KEY = "phc_jBFgac0mVALAFk3negnSfYCcHgvkT00yBLQDmCDYNBb"
|
||||
#REACT_APP_PUBLIC_POSTHOG_UI = "https://us.i.posthog.com"
|
||||
|
||||
#[dev]
|
||||
#command = "npm run dev"
|
||||
#framework = "vite"
|
||||
|
||||
# Set immutable caching for static files, because they have fingerprinted filenames
|
||||
|
||||
[[headers]]
|
||||
for = "/build/*"
|
||||
|
||||
[headers.values]
|
||||
"Cache-Control" = "public, max-age=31560000, immutable"
|
||||
|
||||
# [[redirects]]
|
||||
# from = "/ingest/static/*"
|
||||
# to = "https://us-assets.i.posthog.com/static/:splat"
|
||||
# host = "us-assets.i.posthog.com"
|
||||
# status = 200
|
||||
# force = true
|
||||
#
|
||||
# [[redirects]]
|
||||
# from = "/ingest/*"
|
||||
# to = "https://us.i.posthog.com/:splat"
|
||||
# host = "us.i.posthog.com"
|
||||
# status = 200
|
||||
# force = true
|
||||
-9353
File diff suppressed because it is too large
Load Diff
@@ -1,77 +0,0 @@
|
||||
{
|
||||
"name": "editor2d",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"nx": {
|
||||
"name": "editor2d",
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "echo 'editor2d' && vite",
|
||||
"build_old": "tsc -b && vite build",
|
||||
"build": "vite build",
|
||||
"build2": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/pro-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@preact/signals-react": "^2.2.0",
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"@techstark/opencv-js": "^4.10.0-release.1",
|
||||
"@zip.js/zip.js": "^2.7.52",
|
||||
"dayjs": "^1.11.13",
|
||||
"fortawesome": "^0.0.1-security",
|
||||
"konva": "^9.3.14",
|
||||
"onnxruntime-web": "^1.19.2",
|
||||
"posthog-js": "^1.174.3",
|
||||
"react": "^18.3.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drag-drop-files": "^2.3.10",
|
||||
"react-easy-crop": "^5.1.0",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"sonner": "^2.0.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@preact/signals-react-transform": "^0.4.0",
|
||||
"@tauri-apps/cli": "^2.4.0",
|
||||
"@types/deep-equal": "^1.0.4",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"deep-equal": "^2.2.3",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"globals": "^15.9.0",
|
||||
"i": "^0.3.7",
|
||||
"npm": "^10.8.3",
|
||||
"postcss": "^8.4.41",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.0.1",
|
||||
"vite": "^5.4.1",
|
||||
"vite-plugin-static-copy": "^1.0.6",
|
||||
"vite-tsconfig-paths": "^5.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
import time
|
||||
from io import BytesIO
|
||||
from typing import Dict, Any, Optional, Tuple, Callable, List
|
||||
from dataclasses import dataclass
|
||||
from dataclasses_json import dataclass_json
|
||||
from enum import Enum, auto
|
||||
import base64
|
||||
from PIL import Image
|
||||
import io
|
||||
import torch
|
||||
from diffusers import AutoPipelineForImage2Image, LCMScheduler
|
||||
from diffusers.utils import make_image_grid, load_image
|
||||
from sd_embed.embedding_funcs import get_weighted_text_embeddings_sdxl
|
||||
import torch
|
||||
import websockets
|
||||
from PIL import Image
|
||||
from diffusers import (
|
||||
StableDiffusionXLPipeline,
|
||||
StableDiffusionXLImg2ImgPipeline,
|
||||
AutoPipelineForImage2Image,
|
||||
LCMScheduler,
|
||||
UNet2DConditionModel,
|
||||
)
|
||||
import torch
|
||||
import torchvision.transforms as transforms
|
||||
import gc
|
||||
|
||||
transform = transforms.Compose([
|
||||
transforms.ToTensor(), # This converts PIL Image to tensor and scales to [0, 1]
|
||||
])
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class ProgressMessage:
|
||||
message: str
|
||||
progress: float
|
||||
type: str = "progress"
|
||||
error: Optional[str] = None
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class LoadModelRequest:
|
||||
model_path: str
|
||||
lora_path: Optional[str] = None
|
||||
command: str = "load_model"
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class GenerateRequest:
|
||||
image: str
|
||||
prompt: str
|
||||
command: str = "generate"
|
||||
strength: float = 0.6
|
||||
guidance_scale: float = 2.0
|
||||
num_inference_steps: int = 4
|
||||
height: int = 1024
|
||||
width: int = 1024
|
||||
lora_strength: float = 1.0
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class GenerateResponse:
|
||||
image: str
|
||||
type: str = "result"
|
||||
|
||||
def save_image(image: Image.Image, base_path: str = "outputs") -> str:
|
||||
"""Save a PIL Image to disk with a timestamp-based filename."""
|
||||
try:
|
||||
# Create outputs directory if it doesn't exist
|
||||
output_dir = pathlib.Path(base_path)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Generate filename with timestamp
|
||||
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
||||
filename = f"generated-{timestamp}.png"
|
||||
filepath = output_dir / filename
|
||||
|
||||
# Save the image
|
||||
print(f"Saving image to: {filepath}")
|
||||
image.save(str(filepath), format="PNG")
|
||||
print(f"Successfully saved image to: {filepath}")
|
||||
return str(filepath)
|
||||
except Exception as e:
|
||||
print(f"Error saving image: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
async def send_progress(websocket, message: str, progress: float = 0.0, error: str = None):
|
||||
"""Send progress updates to the client."""
|
||||
progress_msg = ProgressMessage(
|
||||
message=message,
|
||||
progress=progress,
|
||||
error=error
|
||||
)
|
||||
await websocket.send(progress_msg.to_json())
|
||||
|
||||
async def load_model(model_path: str, lora_path: str = None) -> StableDiffusionXLImg2ImgPipeline:
|
||||
"""Load the model with progress reporting."""
|
||||
try:
|
||||
# Load base SDXL pipeline
|
||||
pipe = StableDiffusionXLPipeline.from_single_file(
|
||||
model_path,
|
||||
torch_dtype=torch.float16,
|
||||
use_safetensors=True
|
||||
).to("cuda")
|
||||
|
||||
# Convert to img2img pipeline
|
||||
pipe = StableDiffusionXLImg2ImgPipeline(**pipe.components)
|
||||
|
||||
# Set scheduler
|
||||
pipe.scheduler = LCMScheduler.from_config(pipe.scheduler.config)
|
||||
|
||||
# Load LCM-LoRA weights if provided
|
||||
if lora_path:
|
||||
pipe.load_lora_weights(
|
||||
"F:/ComfyUI_windows_portable_nvidia/ComfyUI_windows_portable/ComfyUI/models/loras/LCM_LoRA_Weights_SDXL.safetensors",
|
||||
adapter_name="lcm"
|
||||
)
|
||||
pipe.load_lora_weights(lora_path, adapter_name="lora")
|
||||
pipe.set_adapters(["lcm", "lora"], adapter_weights=[1.0, 1.0])
|
||||
#pipe.set_adapters(["lcm"], adapter_weights=[1.0])
|
||||
#pipe.unet = torch.compile(pipe.unet, mode="reduce-overhead", fullgraph=True)
|
||||
return pipe
|
||||
except Exception as e:
|
||||
raise Exception(f"Error loading model: {str(e)}")
|
||||
|
||||
#https://huggingface.co/blog/simple_sdxl_optimizations
|
||||
async def generate_image(
|
||||
pipe: StableDiffusionXLImg2ImgPipeline,
|
||||
request: GenerateRequest
|
||||
) -> str:
|
||||
"""Generate image and return base64 string."""
|
||||
try:
|
||||
# Clear CUDA cache before processing
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
image_stream = io.BytesIO(base64.b64decode(request.image))
|
||||
|
||||
init_image = Image.open(image_stream).convert('RGB')
|
||||
init_image = init_image.resize((request.width, request.height)) # width, height
|
||||
init_image = transform(init_image).to('cuda')
|
||||
init_image = init_image.unsqueeze(0)
|
||||
|
||||
(
|
||||
prompt_embeds
|
||||
, prompt_neg_embeds
|
||||
, pooled_prompt_embeds
|
||||
, negative_pooled_prompt_embeds
|
||||
) = get_weighted_text_embeddings_sdxl(
|
||||
pipe,
|
||||
prompt = request.prompt,
|
||||
neg_prompt = "bad quality, poorly Rendered face, poorly drawn face, poorly facial details, poorly drawn hands, poorly rendered hands, low resolution, Images cut out at the top, left, right, bottom, bad composition, mutated body parts, blurry image, disfigured, oversaturated, bad anatomy, deformed body features"
|
||||
)
|
||||
|
||||
# Generate image
|
||||
with torch.inference_mode():
|
||||
output_image = pipe(
|
||||
prompt_embeds = prompt_embeds,
|
||||
negative_prompt_embeds = prompt_neg_embeds,
|
||||
pooled_prompt_embeds = pooled_prompt_embeds,
|
||||
negative_pooled_prompt_embeds = negative_pooled_prompt_embeds,
|
||||
image=init_image,
|
||||
num_inference_steps=request.num_inference_steps,
|
||||
guidance_scale=request.guidance_scale,
|
||||
strength=request.strength,
|
||||
generator=torch.manual_seed(0),
|
||||
height=request.height, width=request.width,
|
||||
).images[0]
|
||||
|
||||
# Clean up CUDA tensors
|
||||
del init_image, prompt_embeds, prompt_neg_embeds,pooled_prompt_embeds, negative_pooled_prompt_embeds
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
|
||||
# Convert to base64
|
||||
buffered = BytesIO()
|
||||
output_image.save(buffered, format="PNG")
|
||||
buffered.seek(0)
|
||||
img_str = base64.b64encode(buffered.getvalue()).decode()
|
||||
|
||||
return f"data:image/png;base64,{img_str}"
|
||||
except Exception as e:
|
||||
raise Exception(f"Error generating image: {str(e)} at line {e.__traceback__.tb_lineno}")
|
||||
|
||||
async def handle_client(websocket):
|
||||
"""Handle WebSocket client connection."""
|
||||
pipe = None
|
||||
try:
|
||||
async for message in websocket:
|
||||
try:
|
||||
# Clear memory before processing new request
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
|
||||
data = json.loads(message)
|
||||
command = data.get("command")
|
||||
|
||||
if command == "load_model":
|
||||
await send_progress(websocket, "Loading model...", 0.0)
|
||||
request = LoadModelRequest.from_json(message)
|
||||
|
||||
if pipe is not None:
|
||||
# Clean up existing model
|
||||
del pipe
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
|
||||
pipe = await load_model(request.model_path, request.lora_path)
|
||||
await send_progress(websocket, "Model loaded successfully", 1.0)
|
||||
|
||||
elif command == "generate":
|
||||
if not pipe:
|
||||
raise Exception("Model not loaded. Please load model first.")
|
||||
|
||||
await send_progress(websocket, "Generating image...", 0.0)
|
||||
request = GenerateRequest.from_json(message)
|
||||
|
||||
# Use a timeout for generation to prevent hanging
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
generate_image(pipe=pipe, request=request),
|
||||
timeout=10.0 # 30 second timeout
|
||||
)
|
||||
response = GenerateResponse(image=result)
|
||||
await websocket.send(response.to_json())
|
||||
except asyncio.TimeoutError:
|
||||
# Force cleanup if generation times out
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
raise Exception("Generation timed out")
|
||||
|
||||
else:
|
||||
raise Exception(f"Unknown command: {command}")
|
||||
|
||||
except Exception as e:
|
||||
await send_progress(websocket, "Error processing request", 0.0, str(e))
|
||||
# Clean up after error
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
print("Client connection closed")
|
||||
except Exception as e:
|
||||
await send_progress(websocket, "Fatal error", 0.0, str(e))
|
||||
finally:
|
||||
# Clean up resources when client disconnects
|
||||
if pipe is not None:
|
||||
del pipe
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
|
||||
async def server():
|
||||
"""Start WebSocket server."""
|
||||
async with websockets.serve(
|
||||
handle_client,
|
||||
"localhost",
|
||||
8765,
|
||||
max_size=None,
|
||||
ping_interval=None, # Disable ping/pong to prevent timeouts
|
||||
max_queue=10 # Limit pending connections
|
||||
):
|
||||
print("Server started on ws://localhost:8765")
|
||||
await asyncio.Future() # run forever
|
||||
|
||||
if __name__ == "__main__":
|
||||
# test() # Comment out test function
|
||||
asyncio.run(server())
|
||||
@@ -1,101 +0,0 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
import time
|
||||
from io import BytesIO
|
||||
from typing import Dict, Any, Optional, Tuple, Callable, List
|
||||
|
||||
import torch
|
||||
from diffusers import AutoPipelineForImage2Image, LCMScheduler
|
||||
from diffusers.utils import make_image_grid, load_image
|
||||
|
||||
import torch
|
||||
import websockets
|
||||
from PIL import Image
|
||||
from diffusers import (
|
||||
StableDiffusionXLPipeline,
|
||||
StableDiffusionXLImg2ImgPipeline,
|
||||
AutoPipelineForImage2Image,
|
||||
LCMScheduler,
|
||||
UNet2DConditionModel,
|
||||
)
|
||||
|
||||
def save_image(image: Image.Image, base_path: str = "outputs") -> str:
|
||||
"""Save a PIL Image to disk with a timestamp-based filename."""
|
||||
try:
|
||||
# Create outputs directory if it doesn't exist
|
||||
output_dir = pathlib.Path(base_path)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Generate filename with timestamp
|
||||
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
||||
filename = f"generated-{timestamp}.png"
|
||||
filepath = output_dir / filename
|
||||
|
||||
# Save the image
|
||||
print(f"Saving image to: {filepath}")
|
||||
image.save(str(filepath), format="PNG")
|
||||
print(f"Successfully saved image to: {filepath}")
|
||||
return str(filepath)
|
||||
except Exception as e:
|
||||
print(f"Error saving image: {e}")
|
||||
return ""
|
||||
|
||||
def test():
|
||||
# Load base SDXL pipeline from .safetensors file
|
||||
pipe = StableDiffusionXLPipeline.from_single_file(
|
||||
f"C:/Users/Tensor/Downloads/animagineXL40_v4Opt.safetensors",
|
||||
torch_dtype=torch.float16,
|
||||
use_safetensors=True,
|
||||
).to("cuda")
|
||||
|
||||
# Convert to img2img pipeline
|
||||
pipe = StableDiffusionXLImg2ImgPipeline(**pipe.components)
|
||||
|
||||
# Set scheduler
|
||||
pipe.scheduler = LCMScheduler.from_config(pipe.scheduler.config)
|
||||
|
||||
# Load LCM-LoRA weights
|
||||
pipe.load_lora_weights(
|
||||
"F:/ComfyUI_windows_portable_nvidia/ComfyUI_windows_portable/ComfyUI/models/loras/LCM_LoRA_Weights_SDXL.safetensors",
|
||||
adapter_name="lcm"
|
||||
)
|
||||
pipe.set_adapters(["lcm"], adapter_weights=[1.0])
|
||||
|
||||
# prepare image
|
||||
url = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/7462a0ac-637d-467b-baa9-34c5f54696b4/original=true,quality=90/60277788.jpeg"
|
||||
init_image = load_image(url)
|
||||
prompt = "safe_pos, 1girl, solo, solo focus, 8k, masterpiece, hires, absurdres, splash art, gradient, looking at viewer, medium breasts, vibrant yellow hair, purple hair tips, (shoulder freckles:1.2), nude, athletic, confident, hand on waist, extremely long hair, dancing, delicate gold jewelry, thin gold bangles, belly button chain, action shot,, head tilt, hair censor, spotlight, tottotonero"
|
||||
|
||||
# pass prompt and image to pipeline
|
||||
generator = torch.manual_seed(0)
|
||||
output_image = pipe(
|
||||
prompt,
|
||||
image=init_image,
|
||||
num_inference_steps=4,
|
||||
guidance_scale=2,
|
||||
strength=0.6,
|
||||
generator=generator
|
||||
).images[0]
|
||||
|
||||
# Save both input and output images
|
||||
save_image(init_image, "outputs/input")
|
||||
output_path = save_image(output_image, "outputs/output")
|
||||
print(f"Output image saved to: {output_path}")
|
||||
|
||||
# Create and save the grid
|
||||
grid = make_image_grid([init_image, output_image], rows=1, cols=2)
|
||||
grid_path = save_image(grid, "outputs/grid")
|
||||
print(f"Grid image saved to: {grid_path}")
|
||||
|
||||
async def start_server():
|
||||
async with websockets.serve(
|
||||
"localhost",
|
||||
8765 # Increase to 10MB
|
||||
):
|
||||
await asyncio.Future() # run forever
|
||||
|
||||
if __name__ == "__main__":
|
||||
test()
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 204 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user