Compare commits

..
Author SHA1 Message Date
Brandon Thomas a0058dc28c plumb controls to backend 2025-07-29 21:14:01 -04:00
bflatastic af02cc6525 number of generation dropdown 2025-07-29 20:18:55 -04:00
Brandon Thomas 4f0916edb7 enqueue multiple images (#749) 2025-07-29 20:14:10 -04:00
bflatastic c36e88d8ce expandable lightbox to fullscreen 2025-07-29 20:11:04 +07:00
Brandon Thomas 233f60acf6 stop double alerting on events 2025-07-28 22:50:02 -04:00
Brandon Thomas a3e16e491a unlikely we'll need old frontend configuration "editor2d_studio_OAI" 2025-07-28 20:04:27 -04:00
Brandon Thomas 3d521fec79 roll forward with new build (07.28) 2025-07-28 10:16:14 -04:00
BFlat 6c8979123f 3d: fixed saving and new scene (#747) 2025-07-28 14:12:56 +07:00
BFlat 8d4edcfafd lightbox context images, generation details, canvas snapshot bug fix (#746)
- canvas snapshot aspect ratio is fixed (it was snapshotting only as
squared images)
- added reference/context images in the lightbox under prompt, hovering
shows a larger preview
- added generation details: model used with logo and provider
2025-07-28 13:28:17 +07:00
Brandon Thomas b53e138dab include prompt details in api docs 2025-07-26 19:10:48 -04:00
Brandon Thomas dc1b038ffa only serve image references for prompts (future proofing) 2025-07-26 18:59:08 -04:00
Brandon Thomas 418a9f92c0 insert prompt images, serve additional metadata (#745) 2025-07-26 18:53:09 -04:00
Brandon Thomas d671f7f951 record prompts on all generate/* andpoints (#744) 2025-07-25 18:29:44 -04:00
bflatastic e97e5c239e lightbox and download button fixes 2025-07-24 17:23:10 +07:00
bflatastic a7270f4685 lightbox prompt fetching 2025-07-24 16:21:21 +07:00
Brandon Thomas a6fb661fbe codegen db 2025-07-24 02:15:22 -04:00
Brandon Thomas e5f5a06b6b prompts code (#743) 2025-07-24 02:01:34 -04:00
614 changed files with 2683 additions and 46642 deletions
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n \n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 13
"Right": 14
},
"nullable": []
},
"hash": "0fa824822cd94024dce21243f41e69e4a3e8685c4de676834a7c0ee14982c976"
"hash": "002cdb3d086f3b4181e2997befa54bd8516cb1d3d3bb6e389862695318d5c299"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO prompts\nSET\n token = ?,\n prompt_type = ?,\n\n maybe_creator_user_token = ?,\n \n maybe_model_type = ?,\n maybe_generation_provider = ?,\n\n maybe_positive_prompt = ?,\n maybe_negative_prompt = ?,\n \n maybe_other_args = ?,\n\n creator_ip_address = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 9
},
"nullable": []
},
"hash": "08f52f15acf6afc7091a525e3dc88f706c71c3579e5e81aa9903807d85f469ef"
}
@@ -0,0 +1,69 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n pci.media_token as `media_token: tokens::tokens::media_files::MediaFileToken`,\n pci.context_semantic_type as `context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension\n\nFROM prompt_context_items pci\nJOIN media_files m\nON\n pci.media_token = m.token\nWHERE\n pci.prompt_token = ?\nORDER BY pci.id ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "media_token: tokens::tokens::media_files::MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 2,
"name": "public_bucket_directory_hash",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "maybe_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "maybe_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
true
]
},
"hash": "094eb099cde85835db0662c2700c6b82644b51bf9b3609391ed4d2945fe5e98f"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO prompts\nSET\n token = ?,\n prompt_type = ?,\n\n maybe_creator_user_token = ?,\n\n maybe_positive_prompt = ?,\n maybe_negative_prompt = ?,\n maybe_other_args = ?,\n\n creator_ip_address = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "13ca06bcbb4a576f8a2229a8554b7488d379baa381d8d5cb60972750e81b2173"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n p.token as `token: tokens::tokens::prompts::PromptToken`,\n\n p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,\n\n p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n\n p.maybe_positive_prompt,\n p.maybe_negative_prompt,\n p.maybe_other_args,\n\n p.creator_ip_address,\n p.created_at\n\nFROM prompts as p\nWHERE\n p.token = ?\n ",
"query": "\nSELECT\n p.token as `token: tokens::tokens::prompts::PromptToken`,\n\n p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,\n\n p.maybe_model_type as `maybe_model_type: enums::common::model_type::ModelType`,\n p.maybe_generation_provider as `maybe_generation_provider: enums::common::generation_provider::GenerationProvider`,\n\n p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n\n p.maybe_positive_prompt,\n p.maybe_negative_prompt,\n p.maybe_other_args,\n\n p.creator_ip_address,\n p.created_at\n\nFROM prompts as p\nWHERE\n p.token = ?\n ",
"describe": {
"columns": [
{
@@ -25,6 +25,26 @@
},
{
"ordinal": 2,
"name": "maybe_model_type: enums::common::model_type::ModelType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "maybe_generation_provider: enums::common::generation_provider::GenerationProvider",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
@@ -34,7 +54,7 @@
}
},
{
"ordinal": 3,
"ordinal": 5,
"name": "maybe_positive_prompt",
"type_info": {
"type": "Blob",
@@ -44,7 +64,7 @@
}
},
{
"ordinal": 4,
"ordinal": 6,
"name": "maybe_negative_prompt",
"type_info": {
"type": "Blob",
@@ -54,7 +74,7 @@
}
},
{
"ordinal": 5,
"ordinal": 7,
"name": "maybe_other_args",
"type_info": {
"type": "Blob",
@@ -64,7 +84,7 @@
}
},
{
"ordinal": 6,
"ordinal": 8,
"name": "creator_ip_address",
"type_info": {
"type": "VarString",
@@ -74,7 +94,7 @@
}
},
{
"ordinal": 7,
"ordinal": 9,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -94,9 +114,11 @@
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "61744bbf83f7cf00348bcc21e868d9c506fec46da4a33ac50d63c6d9e00c0cec"
"hash": "b46b0d022790f4e10f6e47126df0fdc55172bf8cda77d4e0b2afac9e6ebd1630"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n \n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
"describe": {
"columns": [
{
@@ -75,6 +75,16 @@
},
{
"ordinal": 7,
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -84,7 +94,7 @@
}
},
{
"ordinal": 8,
"ordinal": 9,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -105,9 +115,10 @@
true,
true,
false,
true,
false,
false
]
},
"hash": "64dfacc1f0552d539f009fea91e902a2fe296c06ad539808596bbf2c8fc9eaa0"
"hash": "fbe482c8987231c3b5d75f2e792d0fdd033de8a7eb821428fa034752c73edfb7"
}
@@ -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),
+1 -1
View File
@@ -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,
@@ -0,0 +1,123 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::creds::open_ai_api_key::OpenAiApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::prelude::fal_ai::gpt_image_1::edit_image::byok::{gpt_text_to_image, GptTextToImageRequest};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct GptTextToImageByokArgs<'a, V: IntoUrl> {
// Request
pub prompt: &'a str,
pub image_size: GptTextToImageSize,
pub num_images: GptTextToImageNumImages,
pub quality: GptTextToImageQuality,
// Fulfillment
pub api_key: &'a FalApiKey,
pub openai_api_key: &'a OpenAiApiKey,
pub webhook_url: V,
}
#[derive(Copy, Clone, Debug)]
pub enum GptTextToImageSize{
Auto,
Square,
Horizontal,
Vertical,
}
#[derive(Copy, Clone, Debug)]
pub enum GptTextToImageQuality {
Auto,
Low,
Medium,
High,
}
#[derive(Copy, Clone, Debug)]
pub enum GptTextToImageNumImages{
One,
Two,
Three,
Four,
}
pub async fn enqueue_gpt_image_1_text_to_image_webhook<V: IntoUrl>(
args: GptTextToImageByokArgs<'_, V>
) -> Result<WebhookResponse, FalErrorPlus> {
// auto, 1024x1024, 1536x1024, 1024x1536
let image_size = match args.image_size {
GptTextToImageSize::Auto => "auto",
GptTextToImageSize::Square => "1024x1024",
GptTextToImageSize::Horizontal => "1536x1024",
GptTextToImageSize::Vertical => "1024x1536",
};
let quality = match args.quality {
GptTextToImageQuality::Auto => "auto",
GptTextToImageQuality::Low => "low",
GptTextToImageQuality::Medium => "medium",
GptTextToImageQuality::High => "high",
};
let num_images = match args.num_images {
GptTextToImageNumImages::One => 1,
GptTextToImageNumImages::Two => 2,
GptTextToImageNumImages::Three => 3,
GptTextToImageNumImages::Four => 4,
};
let request = GptTextToImageRequest {
prompt: args.prompt.to_string(),
image_size: image_size.to_string(),
num_images,
quality: quality.to_string(),
openai_api_key: args.openai_api_key.0.to_string(),
};
let result = gpt_text_to_image(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::creds::open_ai_api_key::OpenAiApiKey;
use crate::requests::webhook::image::enqueue_gpt_image_1_text_to_image_webhook::{enqueue_gpt_image_1_text_to_image_webhook, GptTextToImageByokArgs, GptTextToImageNumImages, GptTextToImageQuality, GptTextToImageSize};
use errors::AnyhowResult;
use std::fs::read_to_string;
#[tokio::test]
#[ignore]
async fn test_gpt_image_1_edit_image() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let fal_api_key = FalApiKey::from_str(&secret);
let secret = read_to_string("/Users/bt/Artcraft/credentials/openai_api_key.txt")?;
let open_ai_api_key = OpenAiApiKey::from_str(&secret);
let args = GptTextToImageByokArgs {
prompt: "put the man and the ghost on the grassy hill. the man is scared of the friendly ghost.",
api_key: &fal_api_key,
openai_api_key: &open_ai_api_key,
webhook_url: "https://example.com/webhook",
image_size: GptTextToImageSize::Horizontal,
num_images: GptTextToImageNumImages::One,
quality: GptTextToImageQuality::High,
};
let result = enqueue_gpt_image_1_text_to_image_webhook(args).await?;
Ok(())
}
}
@@ -3,4 +3,5 @@ pub mod enqueue_flux_1_schnell_text_to_image_webhook;
pub mod enqueue_flux_pro_11_text_to_image_webhook;
pub mod enqueue_flux_pro_11_ultra_text_to_image_webhook;
pub mod enqueue_gpt_image_1_edit_image_webhook;
pub mod enqueue_gpt_image_1_text_to_image_webhook;
pub mod remove_background_rembg_webhook;
@@ -0,0 +1,19 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageRequest, GenerateGptImage1TextToImageResponse, GPT_IMAGE_1_TEXT_TO_IMAGE_PATH};
pub async fn generate_gpt_image_1_text_to_image(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateGptImage1TextToImageRequest,
) -> Result<GenerateGptImage1TextToImageResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
GPT_IMAGE_1_TEXT_TO_IMAGE_PATH,
maybe_creds,
request,
).await?)
}
@@ -3,4 +3,5 @@ pub mod generate_flux_1_dev_text_to_image;
pub mod generate_flux_1_schnell_text_to_image;
pub mod generate_flux_pro_11_text_to_image;
pub mod generate_flux_pro_11_ultra_text_to_image;
pub mod generate_gpt_image_1_text_to_image;
pub mod remove_image_background;
@@ -1,6 +1,7 @@
use crate::core::commands::enqueue::image::generic::handle_image_artcraft::handle_image_artcraft;
use crate::core::commands::enqueue::image::generic::handle_image_fal::handle_image_fal;
use crate::core::commands::enqueue::image::gpt_image_1::handle_image_sora::handle_image_sora;
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1::handle_gpt_image_1;
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
@@ -39,6 +40,9 @@ pub struct EnqueueTextToImageRequest {
/// Aspect ratio.
pub aspect_ratio: Option<TextToImageSize>,
/// The number of images to generate.
pub number_images: Option<u32>,
}
// TODO(bt,2025-07-14): Support other aspect ratios / resolutions -
@@ -224,7 +228,16 @@ pub async fn dispatch_request(
return Err(InternalImageError::NoModelSpecified);
}
Some(ImageModel::GptImage1) => {
return handle_image_sora(&app, request, sora_creds_manager, sora_task_queue).await;
return handle_gpt_image_1(
request,
app,
app_data_root,
app_env_configs,
provider_priority_store,
storyteller_creds_manager,
sora_creds_manager,
sora_task_queue,
).await;
}
_ => {
// Fall-through
@@ -9,10 +9,10 @@ use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageRequest};
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageRequest};
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageRequest};
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageRequest};
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageNumImages, GenerateFlux1DevTextToImageRequest};
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageNumImages, GenerateFlux1SchnellTextToImageRequest};
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageNumImages, GenerateFluxPro11TextToImageRequest};
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageNumImages, GenerateFluxPro11UltraTextToImageRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
@@ -58,7 +58,7 @@ pub async fn handle_image_artcraft(
return Err(InternalImageError::NoModelSpecified);
}
Some(ImageModel::GptImage1) => {
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: artcraft is handling sora images")));
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: an earlier branch should have handled gpt-image-1 images for Artcraft")));
}
Some(ImageModel::Recraft3) => {
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented in Artcraft")));
@@ -66,19 +66,25 @@ pub async fn handle_image_artcraft(
Some(ImageModel::Flux1Dev) => {
info!("enqueue Flux 1 Dev");
selected_model = GenerationModel::Flux1Dev;
let aspect_ratio = request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
TextToImageSize::Tall => GenerateFlux1DevTextToImageAspectRatio::PortraitNineBySixteen,
TextToImageSize::Wide => GenerateFlux1DevTextToImageAspectRatio::LandscapeSixteenByNine,
TextToImageSize::Auto => GenerateFlux1DevTextToImageAspectRatio::SquareHd,
TextToImageSize::Square => GenerateFlux1DevTextToImageAspectRatio::SquareHd,
});
let request = GenerateFlux1DevTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
aspect_ratio,
num_images: None,
aspect_ratio: request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
TextToImageSize::Tall => GenerateFlux1DevTextToImageAspectRatio::PortraitNineBySixteen,
TextToImageSize::Wide => GenerateFlux1DevTextToImageAspectRatio::LandscapeSixteenByNine,
TextToImageSize::Auto => GenerateFlux1DevTextToImageAspectRatio::SquareHd,
TextToImageSize::Square => GenerateFlux1DevTextToImageAspectRatio::SquareHd,
}),
num_images: request.number_images
.and_then(|num| match num {
1 => Some(GenerateFlux1DevTextToImageNumImages::One),
2 => Some(GenerateFlux1DevTextToImageNumImages::Two),
3 => Some(GenerateFlux1DevTextToImageNumImages::Three),
4 => Some(GenerateFlux1DevTextToImageNumImages::Four),
_ => None, // NB: use service defaults
}),
};
let result = generate_flux_1_dev_text_to_image(
&app_env_configs.storyteller_host,
@@ -99,19 +105,25 @@ pub async fn handle_image_artcraft(
Some(ImageModel::Flux1Schnell) => {
info!("enqueue Flux 1 Schnell");
selected_model = GenerationModel::Flux1Schnell;
let aspect_ratio = request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
TextToImageSize::Tall => GenerateFlux1SchnellTextToImageAspectRatio::PortraitNineBySixteen,
TextToImageSize::Wide => GenerateFlux1SchnellTextToImageAspectRatio::LandscapeSixteenByNine,
TextToImageSize::Auto => GenerateFlux1SchnellTextToImageAspectRatio::SquareHd,
TextToImageSize::Square => GenerateFlux1SchnellTextToImageAspectRatio::SquareHd,
});
let request = GenerateFlux1SchnellTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
aspect_ratio,
num_images: None,
aspect_ratio: request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
TextToImageSize::Tall => GenerateFlux1SchnellTextToImageAspectRatio::PortraitNineBySixteen,
TextToImageSize::Wide => GenerateFlux1SchnellTextToImageAspectRatio::LandscapeSixteenByNine,
TextToImageSize::Square => GenerateFlux1SchnellTextToImageAspectRatio::SquareHd,
TextToImageSize::Auto => GenerateFlux1SchnellTextToImageAspectRatio::SquareHd,
}),
num_images: request.number_images
.and_then(|num| match num {
1 => Some(GenerateFlux1SchnellTextToImageNumImages::One),
2 => Some(GenerateFlux1SchnellTextToImageNumImages::Two),
3 => Some(GenerateFlux1SchnellTextToImageNumImages::Three),
4 => Some(GenerateFlux1SchnellTextToImageNumImages::Four),
_ => None, // NB: use service defaults
}),
};
let result = generate_flux_1_schnell_text_to_image(
&app_env_configs.storyteller_host,
@@ -132,19 +144,25 @@ pub async fn handle_image_artcraft(
Some(ImageModel::FluxPro11) => {
info!("enqueue Flux Pro 1.1");
selected_model = GenerationModel::FluxPro11;
let aspect_ratio = request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
TextToImageSize::Tall => GenerateFluxPro11TextToImageAspectRatio::PortraitNineBySixteen,
TextToImageSize::Wide => GenerateFluxPro11TextToImageAspectRatio::LandscapeSixteenByNine,
TextToImageSize::Auto => GenerateFluxPro11TextToImageAspectRatio::Square,
TextToImageSize::Square => GenerateFluxPro11TextToImageAspectRatio::Square,
});
let request = GenerateFluxPro11TextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
aspect_ratio,
num_images: None,
aspect_ratio: request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
TextToImageSize::Tall => GenerateFluxPro11TextToImageAspectRatio::PortraitNineBySixteen,
TextToImageSize::Wide => GenerateFluxPro11TextToImageAspectRatio::LandscapeSixteenByNine,
TextToImageSize::Square => GenerateFluxPro11TextToImageAspectRatio::Square,
TextToImageSize::Auto => GenerateFluxPro11TextToImageAspectRatio::Square,
}),
num_images: request.number_images
.and_then(|num| match num {
1 => Some(GenerateFluxPro11TextToImageNumImages::One),
2 => Some(GenerateFluxPro11TextToImageNumImages::Two),
3 => Some(GenerateFluxPro11TextToImageNumImages::Three),
4 => Some(GenerateFluxPro11TextToImageNumImages::Four),
_ => None, // NB: use service defaults
}),
};
let result = generate_flux_pro_11_text_to_image(
&app_env_configs.storyteller_host,
@@ -165,19 +183,25 @@ pub async fn handle_image_artcraft(
Some(ImageModel::FluxPro11Ultra) => {
info!("enqueue Flux Pro 1.1 Ultra");
selected_model = GenerationModel::FluxPro11Ultra;
let aspect_ratio = request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
TextToImageSize::Tall => GenerateFluxPro11UltraTextToImageAspectRatio::PortraitNineBySixteen,
TextToImageSize::Wide => GenerateFluxPro11UltraTextToImageAspectRatio::LandscapeSixteenByNine,
TextToImageSize::Auto => GenerateFluxPro11UltraTextToImageAspectRatio::Square,
TextToImageSize::Square => GenerateFluxPro11UltraTextToImageAspectRatio::Square,
});
let request = GenerateFluxPro11UltraTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
aspect_ratio,
num_images: None,
aspect_ratio: request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
TextToImageSize::Tall => GenerateFluxPro11UltraTextToImageAspectRatio::PortraitNineBySixteen,
TextToImageSize::Wide => GenerateFluxPro11UltraTextToImageAspectRatio::LandscapeSixteenByNine,
TextToImageSize::Square => GenerateFluxPro11UltraTextToImageAspectRatio::Square,
TextToImageSize::Auto => GenerateFluxPro11UltraTextToImageAspectRatio::Square,
}),
num_images: request.number_images
.and_then(|num| match num {
1 => Some(GenerateFluxPro11UltraTextToImageNumImages::One),
2 => Some(GenerateFluxPro11UltraTextToImageNumImages::Two),
3 => Some(GenerateFluxPro11UltraTextToImageNumImages::Three),
4 => Some(GenerateFluxPro11UltraTextToImageNumImages::Four),
_ => None, // NB: use service defaults
}),
};
let result = generate_flux_pro_11_ultra_text_to_image(
&app_env_configs.storyteller_host,
@@ -0,0 +1,75 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::model::image_models::ImageModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use log::{error, info};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use std::time::Duration;
use tauri::AppHandle;
pub async fn handle_gpt_image_1(
request: EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
provider_priority_store: &ProviderPriorityStore,
storyteller_creds_manager: &StorytellerCredentialManager,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
let priority = provider_priority_store.get_priority()?;
// TODO: Check if providers are available before proceeding.
info!("Providers by priority: {:?}", priority);
for provider in priority.iter() {
match provider {
Provider::Fal => {
// Fallthrough
// If in the future we support OpenAI API keys, it's worth considering whether we
// send those requests ourselves, or if we use FAL as an intermediary. FAL makes
// the API nicer to deal with, but the user needs an additional key.
}
Provider::Artcraft => {
info!("Dispatching gpt-image-1 via Artcraft...");
return handle_gpt_image_1_artcraft(
request,
app_env_configs,
storyteller_creds_manager
).await;
}
Provider::Sora => {
info!("Dispatching gpt-image-1 via Sora...");
return handle_gpt_image_1_sora(
request,
app,
sora_creds_manager,
sora_task_queue,
).await;
}
}
}
Err(InternalImageError::NoProviderAvailable)
}
@@ -0,0 +1,115 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::model::image_models::ImageModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use std::time::Duration;
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
use storyteller_client::generate::image::generate_gpt_image_1_text_to_image::generate_gpt_image_1_text_to_image;
use tauri::AppHandle;
pub async fn handle_gpt_image_1_artcraft(
request: EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(InternalImageError::NeedsStorytellerCredentials);
},
};
info!("Calling Artcraft gpt-image-1 ...");
let uuid_idempotency_token = generate_random_uuid();
let image_quality = Some(GenerateGptImage1TextToImageImageQuality::High);
//let image_quality = request.image_quality
// .map(|quality| match quality {
// EditImageQuality::Auto => GenerateGptImage1TextToImageImageQuality::Auto,
// EditImageQuality::High => GenerateGptImage1TextToImageImageQuality::High,
// EditImageQuality::Medium => GenerateGptImage1TextToImageImageQuality::Medium,
// EditImageQuality::Low => GenerateGptImage1TextToImageImageQuality::Low,
// });
let image_size = request.aspect_ratio
.map(|size| match size {
TextToImageSize::Auto => GenerateGptImage1TextToImageImageSize::Square,
TextToImageSize::Square => GenerateGptImage1TextToImageImageSize::Square,
TextToImageSize::Tall => GenerateGptImage1TextToImageImageSize::Vertical,
TextToImageSize::Wide => GenerateGptImage1TextToImageImageSize::Horizontal,
});
let num_images = match request.number_images {
None => None,
Some(1) => Some(GenerateGptImage1TextToImageNumImages::One),
Some(2) => Some(GenerateGptImage1TextToImageNumImages::Two),
Some(3) => Some(GenerateGptImage1TextToImageNumImages::Three),
Some(4) => Some(GenerateGptImage1TextToImageNumImages::Four),
// TODO: Error
//Some(other) => {
// return Err(InternalImageError::InvalidNumberOfRequestedImages {
// min: 1,
// max: 4,
// requested: other,
// });
//},
_ => Some(GenerateGptImage1TextToImageNumImages::One), // Default to one image if invalid number
};
let request = GenerateGptImage1TextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
image_size,
num_images,
image_quality,
};
let result = generate_gpt_image_1_text_to_image(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft gpt-image-1. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
return Err(InternalImageError::StorytellerError(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::GptImage1),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
})
}
@@ -19,9 +19,9 @@ use tauri::AppHandle;
const SORA_SIMPLE_IMAGE_GEN_TIMEOUT : Duration = Duration::from_millis(1000 * 30); // 30 seconds
pub async fn handle_image_sora(
app: &AppHandle,
pub async fn handle_gpt_image_1_sora(
request: EnqueueTextToImageRequest,
app: &AppHandle,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
@@ -55,10 +55,19 @@ pub async fn handle_image_sora(
Some(TextToImageSize::Wide) => ImageSize::Wide,
};
let num_images = match request.number_images {
None => NumImages::One,
Some(1) => NumImages::One,
Some(2) => NumImages::Two,
Some(3) => NumImages::Two, // NB: There is no option for three images!
Some(4) => NumImages::Four,
_ => NumImages::One,
};
let result =
simple_image_gen_with_session_auto_renew(SimpleImageGenAutoRenewRequest {
prompt: prompt.to_string(),
num_images: NumImages::One,
num_images,
image_size: size,
credentials: &creds,
request_timeout: Some(SORA_SIMPLE_IMAGE_GEN_TIMEOUT),
@@ -1 +1,3 @@
pub mod handle_image_sora;
pub (crate) mod handle_gpt_image_1_artcraft;
pub (crate) mod handle_gpt_image_1_sora;
pub (super) mod handle_gpt_image_1;
@@ -3,7 +3,7 @@ use crate::core::commands::enqueue::image::internal_image_error::InternalImageEr
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal::handle_generic_bg_removal;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::handle_gpt_image_1;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
@@ -4,8 +4,8 @@ use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal_fal::handle_generic_bg_removal_fal;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, EnqueueTextToImageSuccessResponse};
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::handle_gpt_image_1;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
@@ -208,7 +208,7 @@ pub async fn handle_request(
return Err(InternalContextualEditImageError::NoModelSpecified)
}
Some(ContextualImageEditModel::GptImage1) => {
handle_gpt_image_1(
handle_gpt_image_1_edit(
request,
app,
app_data_root,
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
@@ -16,7 +16,7 @@ use tauri::AppHandle;
pub(super) const MAX_IMAGES: usize = 10;
pub async fn handle_gpt_image_1(
pub async fn handle_gpt_image_1_edit(
request: &EnqueueContextualEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
@@ -44,8 +44,8 @@ pub async fn handle_gpt_image_1(
// the API nicer to deal with, but the user needs an additional key.
}
Provider::Artcraft => {
info!("Dispatching gpt-image-1 via Artcraft...");
return handle_gpt_image_1_artcraft(
info!("Dispatching gpt-image-1 (edit) via Artcraft...");
return handle_gpt_image_1_edit_artcraft(
request,
app,
app_data_root,
@@ -54,8 +54,8 @@ pub async fn handle_gpt_image_1(
).await;
}
Provider::Sora => {
info!("Dispatching gpt-image-1 via Sora...");
return handle_gpt_image_1_sora(
info!("Dispatching gpt-image-1 (edit) via Sora...");
return handle_gpt_image_1_edit_sora(
request,
app,
app_data_root,
@@ -2,7 +2,7 @@ use crate::core::commands::enqueue::image::enqueue_text_to_image_command::Enqueu
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::MAX_IMAGES;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::MAX_IMAGES;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
@@ -36,7 +36,7 @@ use tauri::AppHandle;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
pub async fn handle_gpt_image_1_artcraft(
pub async fn handle_gpt_image_1_edit_artcraft(
request: &EnqueueContextualEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
@@ -51,7 +51,7 @@ pub async fn handle_gpt_image_1_artcraft(
},
};
info!("Calling Artcraft gpt-image-1 ...");
info!("Calling Artcraft gpt-image-1 (edit) ...");
let uuid_idempotency_token = generate_random_uuid();
@@ -2,7 +2,7 @@ use crate::core::commands::enqueue::image::enqueue_text_to_image_command::Enqueu
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageSize, EnqueueContextualEditImageCommand, EnqueueContextualEditImageErrorType};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::MAX_IMAGES;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::MAX_IMAGES;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -48,7 +48,7 @@ const SORA_IMAGE_REMIX_TIMEOUT: Duration = Duration::from_millis(1000 * 30); //
const DEFAULT_ASPECT_RATIO : EditImageSize = EditImageSize::Square;
pub async fn handle_gpt_image_1_sora(
pub async fn handle_gpt_image_1_edit_sora(
request: &EnqueueContextualEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
@@ -1,3 +1,3 @@
pub mod handle_gpt_image_1;
pub mod handle_gpt_image_1_artcraft;
pub mod handle_gpt_image_1_sora;
pub mod handle_gpt_image_1_edit;
pub mod handle_gpt_image_1_edit_artcraft;
pub mod handle_gpt_image_1_edit_sora;
+1 -1
View File
@@ -26,7 +26,7 @@ errors = { workspace = true}
mimetypes = { workspace = true }
# External
image = "0.25.6"
image = { version = "0.25.6" , features = ["jpeg", "png", "tiff", "webp"] }
[dev-dependencies]
anyhow = { workspace = true }
@@ -0,0 +1 @@
pub mod webp_bytes_to_png_bytes;
@@ -0,0 +1,21 @@
use crate::error::ImagesError;
use image::{ImageFormat, ImageReader};
use std::io::Cursor;
pub fn webp_bytes_to_png_bytes(
webp_bytes: &[u8],
) -> Result<Vec<u8>, ImagesError> {
let mut reader = ImageReader::new(Cursor::new(webp_bytes));
reader.set_format(ImageFormat::WebP); // Default to WebP format (as fallback).
let image = reader
.with_guessed_format()? // NB: Can raise IoError.
.decode()?;
let mut output_bytes: Vec<u8> = Vec::new();
image.write_to(&mut Cursor::new(&mut output_bytes), ImageFormat::Png)?;
Ok(output_bytes)
}
+31
View File
@@ -0,0 +1,31 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum ImagesError {
IoError(std::io::Error),
ImageError(image::ImageError),
}
impl Error for ImagesError {}
impl Display for ImagesError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ImagesError::IoError(err) => write!(f, "IO Error: {}", err),
ImagesError::ImageError(err) => write!(f, "Image Error: {}", err),
}
}
}
impl From<std::io::Error> for ImagesError {
fn from(err: std::io::Error) -> Self {
ImagesError::IoError(err)
}
}
impl From<image::ImageError> for ImagesError {
fn from(err: image::ImageError) -> Self {
ImagesError::ImageError(err)
}
}
+2
View File
@@ -23,6 +23,8 @@
// Re-export image to everyone importing this library
pub use image;
pub mod encoding;
pub mod error;
pub mod image_info;
pub mod resize_image_file_preserving_aspect;
pub mod resize_preserving_aspect;
@@ -21,6 +21,7 @@ pub mod media_uploads;
pub mod model_categories;
pub mod model_weight_usage_counts;
pub mod model_weights;
pub mod prompt_context_items;
pub mod prompts;
pub mod public_event_feed;
pub mod stats;
@@ -0,0 +1,58 @@
use anyhow::anyhow;
use log::error;
use sqlx::{MySql, QueryBuilder, Transaction};
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use errors::AnyhowResult;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
pub struct PromptContextItem {
pub media_token: MediaFileToken,
pub context_semantic_type: PromptContextSemanticType,
}
pub struct InsertBatchArgs<'a, 'b> {
pub prompt_token: PromptToken,
pub items: Vec<PromptContextItem>,
pub transaction: &'a mut Transaction<'b, MySql>,
}
/// Insert a list of entities into a "batch" together for grouping; returns the new batch token identifier to return
/// to the HTTP caller.
///
/// NB: Calling code is responsible for rolling back the transaction if this fails.
pub async fn insert_batch_prompt_context_items<'a, 'b>(args: InsertBatchArgs<'a, 'b>) -> AnyhowResult<()> {
let mut query_builder = QueryBuilder::new(r#"
INSERT INTO prompt_context_items (prompt_token, media_token, context_semantic_type) VALUES
"#);
for (i, item) in args.items.iter().enumerate() {
query_builder.push("(");
query_builder.push_bind(&args.prompt_token);
query_builder.push(",");
query_builder.push_bind(item.media_token.to_string());
query_builder.push(",");
query_builder.push_bind(item.context_semantic_type.to_string());
query_builder.push(")");
if i < args.items.len() - 1 {
query_builder.push(",");
}
}
let query = query_builder.build();
let query_result = query.execute(&mut **args.transaction).await;
match query_result {
Ok(_) => Ok(()),
Err(err) => {
error!("Error with prompt context item insert query: {:?}", &err);
Err(anyhow!("prompt context item insert error: {:?}", &err))
},
}
}
@@ -0,0 +1,75 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use errors::AnyhowResult;
use log::{info, warn};
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
pub struct PromptContextItem {
pub media_token: MediaFileToken,
pub context_semantic_type: PromptContextSemanticType,
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
}
struct RawPromptContextItem {
media_token: MediaFileToken,
context_semantic_type: PromptContextSemanticType,
public_bucket_directory_hash: String,
maybe_public_bucket_prefix: Option<String>,
maybe_public_bucket_extension: Option<String>,
}
pub async fn list_prompt_context_items(
prompt_token: &PromptToken,
mysql_connection: &mut PoolConnection<MySql>,
) -> AnyhowResult<Vec<PromptContextItem>> {
let result = sqlx::query_as!(
RawPromptContextItem,
r#"
SELECT
pci.media_token as `media_token: tokens::tokens::media_files::MediaFileToken`,
pci.context_semantic_type as `context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType`,
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension
FROM prompt_context_items pci
JOIN media_files m
ON
pci.media_token = m.token
WHERE
pci.prompt_token = ?
ORDER BY pci.id ASC
"#,
prompt_token.as_str()
)
.fetch_all(&mut **mysql_connection)
.await;
match result {
Ok(items) => {
Ok(items.into_iter().map(|item| {
PromptContextItem {
media_token: item.media_token,
context_semantic_type: item.context_semantic_type,
public_bucket_directory_hash: item.public_bucket_directory_hash,
maybe_public_bucket_prefix: item.maybe_public_bucket_prefix,
maybe_public_bucket_extension: item.maybe_public_bucket_extension,
}
}).collect::<Vec<PromptContextItem>>())
}
Err(err) => match err {
sqlx::Error::RowNotFound => Ok(Vec::new()),
_ => {
warn!("Error querying prompt context items: {:?}", err);
Err(anyhow!("error querying prompt context items"))
}
}
}
}
@@ -0,0 +1,2 @@
pub mod insert_batch_prompt_context_items;
pub mod list_prompt_context_items;
@@ -5,10 +5,12 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use sqlx::MySqlPool;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use errors::AnyhowResult;
use sqlx::pool::PoolConnection;
use sqlx::{MySql, MySqlPool};
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
@@ -20,6 +22,9 @@ pub struct Prompt {
pub prompt_type: PromptType,
pub maybe_model_type: Option<ModelType>,
pub maybe_generation_provider: Option<GenerationProvider>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_positive_prompt: Option<String>,
@@ -39,6 +44,9 @@ pub struct PromptRaw {
pub prompt_type: PromptType,
pub maybe_model_type: Option<ModelType>,
pub maybe_generation_provider: Option<GenerationProvider>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_positive_prompt: Option<String>,
@@ -52,12 +60,21 @@ pub struct PromptRaw {
pub created_at: DateTime<Utc>,
}
pub async fn get_prompt(
prompt_token: &PromptToken,
mysql_pool: &MySqlPool
) -> AnyhowResult<Option<Prompt>> {
let mut connection = mysql_pool.acquire().await?;
get_prompt_from_connection(prompt_token, &mut connection).await
}
let record = select_record(prompt_token, mysql_pool).await;
pub async fn get_prompt_from_connection(
prompt_token: &PromptToken,
mysql_connection: &mut PoolConnection<MySql>
) -> AnyhowResult<Option<Prompt>> {
let record = select_record(prompt_token, mysql_connection).await;
let record = match record {
Ok(record) => record,
@@ -72,6 +89,8 @@ pub async fn get_prompt(
Ok(Some(Prompt {
token: record.token,
prompt_type: record.prompt_type,
maybe_model_type: record.maybe_model_type,
maybe_generation_provider: record.maybe_generation_provider,
maybe_creator_user_token: record.maybe_creator_user_token,
maybe_positive_prompt: record.maybe_positive_prompt,
maybe_negative_prompt: record.maybe_negative_prompt,
@@ -86,7 +105,7 @@ pub async fn get_prompt(
async fn select_record(
prompt_token: &PromptToken,
mysql_pool: &MySqlPool
mysql_connection: &mut PoolConnection<MySql>
) -> Result<PromptRaw, sqlx::Error> {
sqlx::query_as!(
PromptRaw,
@@ -96,6 +115,9 @@ SELECT
p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,
p.maybe_model_type as `maybe_model_type: enums::common::model_type::ModelType`,
p.maybe_generation_provider as `maybe_generation_provider: enums::common::generation_provider::GenerationProvider`,
p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
p.maybe_positive_prompt,
@@ -111,6 +133,6 @@ WHERE
"#,
prompt_token
)
.fetch_one(mysql_pool)
.fetch_one(&mut **mysql_connection)
.await
}
@@ -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,
@@ -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::*;
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -0,0 +1,213 @@
use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest, GenerateGptImage1TextToImageResponse};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
use fal_client::requests::webhook::image::enqueue_gpt_image_1_text_to_image_webhook::{enqueue_gpt_image_1_text_to_image_webhook, GptTextToImageByokArgs, GptTextToImageNumImages, GptTextToImageQuality, GptTextToImageSize};
use http_server_common::request::get_request_ip::get_request_ip;
use log::{error, info, warn};
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use utoipa::ToSchema;
/// Gpt Image 1
#[utoipa::path(
post,
tag = "Generate Images",
path = "/v1/generate/image/gpt_image_1_text_to_image",
responses(
(status = 200, description = "Success", body = GenerateGptImage1TextToImageResponse),
),
params(
("request" = GenerateGptImage1TextToImageRequest, description = "Payload for Request"),
)
)]
pub async fn generate_gpt_image_1_text_to_image_handler(
http_request: HttpRequest,
request: Json<GenerateGptImage1TextToImageRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateGptImage1TextToImageResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
// Some(session) => session,
// None => {
// warn!("not logged in");
// return Err(CommonWebError::NotAuthorized);
// }
//};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
const CAN_SEE_DELETED: bool = false;
let media_domain = get_media_domain(&http_request);
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
})?;
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let image_size = match request.image_size {
Some(GenerateGptImage1TextToImageImageSize::Square) => GptTextToImageSize::Square,
Some(GenerateGptImage1TextToImageImageSize::Horizontal) => GptTextToImageSize::Horizontal,
Some(GenerateGptImage1TextToImageImageSize::Vertical) => GptTextToImageSize::Vertical,
None => GptTextToImageSize::Square, // Default to Square
};
let num_images = match request.num_images {
Some(GenerateGptImage1TextToImageNumImages::One) => GptTextToImageNumImages::One,
Some(GenerateGptImage1TextToImageNumImages::Two) => GptTextToImageNumImages::Two,
Some(GenerateGptImage1TextToImageNumImages::Three) => GptTextToImageNumImages::Three,
Some(GenerateGptImage1TextToImageNumImages::Four) => GptTextToImageNumImages::Four,
None => GptTextToImageNumImages::One, // Default to One
};
let quality = match request.image_quality {
Some(quality) => match quality {
GenerateGptImage1TextToImageImageQuality::Auto => GptTextToImageQuality::Auto,
GenerateGptImage1TextToImageImageQuality::Low => GptTextToImageQuality::Low,
GenerateGptImage1TextToImageImageQuality::Medium => GptTextToImageQuality::Medium,
GenerateGptImage1TextToImageImageQuality::High => GptTextToImageQuality::High,
},
None => GptTextToImageQuality::High, // Default to High
};
let openai_api_key = OpenAiApiKey::from_str(&server_state.openai.api_key);
let args = GptTextToImageByokArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
num_images,
image_size,
quality,
openai_api_key: &openai_api_key,
};
let fal_result = enqueue_gpt_image_1_text_to_image_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_gpt_image_1_text_to_image_webhook: {:?}", err);
CommonWebError::ServerError
})?;
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
CommonWebError::ServerError
})?;
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::GptImage1),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GenerateGptImage1TextToImageResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -3,4 +3,5 @@ pub mod generate_flux_1_dev_text_to_image_handler;
pub mod generate_flux_1_schnell_text_to_image_handler;
pub mod generate_flux_pro_11_text_to_image_handler;
pub mod generate_flux_pro_11_ultra_text_to_image_handler;
pub mod generate_gpt_image_1_text_to_image_handler;
pub mod remove_image_background_handler;
@@ -10,6 +10,7 @@ use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundRequest;
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundResponse;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::image::remove_background_rembg_webhook::{remove_background_rembg_webhook, RemoveBackgroundRembgWebhookArgs};
use http_server_common::request::get_request_ip::get_request_ip;
@@ -18,7 +19,9 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file_with_connection;
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use sqlx::Acquire;
use utoipa::ToSchema;
/// Background removal
@@ -38,9 +41,13 @@ pub async fn remove_image_background_handler(
request: Json<RemoveImageBackgroundRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<RemoveImageBackgroundResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -73,7 +80,7 @@ pub async fn remove_image_background_handler(
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
@@ -81,10 +88,10 @@ pub async fn remove_image_background_handler(
})?;
const IS_MOD : bool = false;
let media_file_lookup_result = get_media_file(
let media_file_lookup_result = get_media_file_with_connection(
media_file_token,
IS_MOD,
&server_state.mysql_pool,
&mut mysql_connection,
).await;
let media_file = match media_file_lookup_result {
@@ -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;
@@ -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;
@@ -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;
@@ -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,
@@ -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;
@@ -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,
@@ -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,
@@ -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
@@ -1,22 +1,28 @@
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Path;
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
use artcraft_api_defs::common::responses::media_links::MediaLinks;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use chrono::{DateTime, Utc};
use log::warn;
use utoipa::ToSchema;
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use mysql_queries::queries::prompts::get_prompt::get_prompt;
use log::{error, warn};
use mysql_queries::queries::prompt_context_items::list_prompt_context_items::list_prompt_context_items;
use mysql_queries::queries::prompts::get_prompt::{get_prompt, get_prompt_from_connection};
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use utoipa::ToSchema;
/// For the URL PathInfo
#[derive(Deserialize, ToSchema)]
@@ -30,6 +36,13 @@ pub struct GetPromptSuccessResponse {
pub prompt: PromptInfo,
}
#[derive(Serialize, ToSchema)]
pub struct GetPromptImageContextItem {
pub media_token: MediaFileToken,
pub semantic: PromptContextSemanticType,
pub media_links: MediaLinks,
}
#[derive(Serialize, ToSchema)]
pub struct PromptInfo {
pub token: PromptToken,
@@ -37,6 +50,12 @@ pub struct PromptInfo {
/// The type of prompt.
/// Note: Prompts may or may not be compatible across systems.
pub prompt_type: PromptType,
/// The type of model used
pub maybe_model_type: Option<ModelType>,
/// The service provider used
pub maybe_generation_provider: Option<GenerationProvider>,
/// Positive prompt (technically optional, but usually present)
pub maybe_positive_prompt: Option<String>,
@@ -44,6 +63,9 @@ pub struct PromptInfo {
/// Negative prompt (optional)
pub maybe_negative_prompt: Option<String>,
/// Context images (optional)
pub maybe_context_images: Option<Vec<GetPromptImageContextItem>>,
/// Scheduled / travel prompt (optional)
pub maybe_travel_prompt: Option<String>,
@@ -165,9 +187,17 @@ pub async fn get_prompt_handler(
path: Path<GetPromptPathInfo>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, GetPromptError>
{
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
error!("Error acquiring MySQL connection: {:?}", err);
GetPromptError::ServerError
})?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -180,9 +210,9 @@ pub async fn get_prompt_handler(
let prompt_token = path.into_inner().token;
let result = get_prompt(
let result = get_prompt_from_connection(
&prompt_token,
&server_state.mysql_pool
&mut mysql_connection
).await;
let result = match result {
@@ -247,13 +277,64 @@ pub async fn get_prompt_handler(
});
}
let media_domain = get_media_domain(&http_request);
let items_result = list_prompt_context_items(
&result.token,
&mut mysql_connection
).await;
let items = items_result.unwrap_or_else(|e| {
warn!("Error listing prompt context items: {:?}", e);
Vec::new()
});
let items = items.iter().filter_map(|item| {
match item.context_semantic_type {
PromptContextSemanticType::VidStartFrame => {},
PromptContextSemanticType::VidEndFrame => {},
PromptContextSemanticType::Imgref => {},
PromptContextSemanticType::ImgrefCharacter => {},
PromptContextSemanticType::ImgrefStyle => {},
PromptContextSemanticType::ImgrefBg => {},
_ => {
// NB: Only return images. In the future we may add context items for stages, persisted data, etc.
return None
},
}
let bucket_path = MediaFileBucketPath::from_object_hash(
&item.public_bucket_directory_hash,
item.maybe_public_bucket_prefix.as_deref(),
item.maybe_public_bucket_extension.as_deref());
Some(GetPromptImageContextItem {
media_token: item.media_token.clone(),
semantic: item.context_semantic_type,
media_links: MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&bucket_path,
),
})
}).collect::<Vec<GetPromptImageContextItem>>();
let maybe_context_images = if items.is_empty() {
None
} else {
Some(items)
};
let response = GetPromptSuccessResponse {
success: true,
prompt: PromptInfo {
token: result.token,
maybe_strength,
maybe_model_type: result.maybe_model_type,
maybe_generation_provider: result.maybe_generation_provider,
maybe_positive_prompt: result.maybe_positive_prompt,
maybe_negative_prompt: result.maybe_negative_prompt,
maybe_context_images,
maybe_travel_prompt,
maybe_style_name,
maybe_inference_duration_millis,
@@ -7,8 +7,10 @@ use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCat
use enums::by_table::media_files::media_file_type::MediaFileType;
use errors::AnyhowResult;
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
use images::encoding::webp_bytes_to_png_bytes::webp_bytes_to_png_bytes;
use images::image_info::image_info::ImageInfo;
use log::info;
use mimetypes::mimetype_info::file_extension::FileExtension;
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
@@ -32,18 +34,17 @@ pub async fn handle_image_payload(
job: &FalJobDetails,
server_state: &ServerState,
) -> AnyhowResult<MediaFileToken> {
let image_value = payload.get("image")
.ok_or_else(|| anyhow!("no `image` key in payload"))?;
info!("Fal Image Payload: {:?}", image_value);
let image: FalWebhookImage = serde_json::from_value(image_value.clone())?;
let image_url = image.url
.as_deref()
.ok_or_else(|| anyhow!("no `url` in image payload"))?;
//let mime_type = image.content_type
// .as_deref()
// .ok_or_else(|| anyhow!("no `content_type` in image payload"))?;
@@ -51,10 +52,53 @@ pub async fn handle_image_payload(
let file_bytes = http_download_url_to_bytes(image_url)
.await
.map_err(|e| anyhow!("Failed to download image: {:?}", e))?;
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
info!("File type: {}, extension: {:?}",
mimetype_info.mime_type(),
mimetype_info.file_extension());
match mimetype_info.file_extension() {
Some(FileExtension::Webp) => {
info!("Artcraft can't handle WebP images yet; converting to PNG...");
let file_bytes = webp_bytes_to_png_bytes(&file_bytes)?;
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
info!("Updated file type: {}, extension: {:?}",
mimetype_info.mime_type(),
mimetype_info.file_extension());
upload_single_image_bytes(
job,
server_state,
&file_bytes,
mimetype_info,
).await
}
_ => {
upload_single_image_bytes(
job,
server_state,
&file_bytes,
mimetype_info,
).await
}
}
}
async fn upload_single_image_bytes(
job: &FalJobDetails,
server_state: &ServerState,
file_bytes: &[u8],
mimetype_info: MimetypeInfo,
) -> AnyhowResult<MediaFileToken> {
let mime_type = mimetype_info.mime_type();
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
@@ -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);
}
};
}, []);
};
@@ -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&apos;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>
);
@@ -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 */}
</>
);
}
+4 -5
View File
@@ -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 -----
+1
View File
@@ -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
-3
View File
@@ -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
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