work in progress multiple image batches, having to add new gpt-image-1

This commit is contained in:
Brandon Thomas committed 2025-07-29 16:28:10 -04:00
1 parent c36e88d8ce
commit b1735eb38c
23 files changed
+587 -30

No files matched your search

@@ -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
@@ -0,0 +1,75 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::model::image_models::ImageModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use log::{error, info};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use std::time::Duration;
use tauri::AppHandle;
pub async fn handle_gpt_image_1(
request: EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
provider_priority_store: &ProviderPriorityStore,
storyteller_creds_manager: &StorytellerCredentialManager,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
let priority = provider_priority_store.get_priority()?;
// TODO: Check if providers are available before proceeding.
info!("Providers by priority: {:?}", priority);
for provider in priority.iter() {
match provider {
Provider::Fal => {
// Fallthrough
// If in the future we support OpenAI API keys, it's worth considering whether we
// send those requests ourselves, or if we use FAL as an intermediary. FAL makes
// the API nicer to deal with, but the user needs an additional key.
}
Provider::Artcraft => {
info!("Dispatching gpt-image-1 via Artcraft...");
return handle_gpt_image_1_artcraft(
request,
app_env_configs,
storyteller_creds_manager
).await;
}
Provider::Sora => {
info!("Dispatching gpt-image-1 via Sora...");
return handle_gpt_image_1_sora(
request,
app,
sora_creds_manager,
sora_task_queue,
).await;
}
}
}
Err(InternalImageError::NoProviderAvailable)
}
@@ -0,0 +1,115 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::model::image_models::ImageModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use std::time::Duration;
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
use storyteller_client::generate::image::generate_gpt_image_1_text_to_image::generate_gpt_image_1_text_to_image;
use tauri::AppHandle;
pub async fn handle_gpt_image_1_artcraft(
request: EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(InternalImageError::NeedsStorytellerCredentials);
},
};
info!("Calling Artcraft gpt-image-1 ...");
let uuid_idempotency_token = generate_random_uuid();
let image_quality = Some(GenerateGptImage1TextToImageImageQuality::High);
//let image_quality = request.image_quality
// .map(|quality| match quality {
// EditImageQuality::Auto => GenerateGptImage1TextToImageImageQuality::Auto,
// EditImageQuality::High => GenerateGptImage1TextToImageImageQuality::High,
// EditImageQuality::Medium => GenerateGptImage1TextToImageImageQuality::Medium,
// EditImageQuality::Low => GenerateGptImage1TextToImageImageQuality::Low,
// });
let image_size = request.aspect_ratio
.map(|size| match size {
TextToImageSize::Auto => GenerateGptImage1TextToImageImageSize::Square,
TextToImageSize::Square => GenerateGptImage1TextToImageImageSize::Square,
TextToImageSize::Tall => GenerateGptImage1TextToImageImageSize::Vertical,
TextToImageSize::Wide => GenerateGptImage1TextToImageImageSize::Horizontal,
});
let num_images = match request.number_images {
None => None,
Some(1) => Some(GenerateGptImage1TextToImageNumImages::One),
Some(2) => Some(GenerateGptImage1TextToImageNumImages::Two),
Some(3) => Some(GenerateGptImage1TextToImageNumImages::Three),
Some(4) => Some(GenerateGptImage1TextToImageNumImages::Four),
// TODO: Error
//Some(other) => {
// return Err(InternalImageError::InvalidNumberOfRequestedImages {
// min: 1,
// max: 4,
// requested: other,
// });
//},
_ => Some(GenerateGptImage1TextToImageNumImages::One), // Default to one image if invalid number
};
let request = GenerateGptImage1TextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
image_size,
num_images,
image_quality,
};
let result = generate_gpt_image_1_text_to_image(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft gpt-image-1. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
return Err(InternalImageError::StorytellerError(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::GptImage1),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
})
}
@@ -19,9 +19,9 @@ use tauri::AppHandle;
const SORA_SIMPLE_IMAGE_GEN_TIMEOUT : Duration = Duration::from_millis(1000 * 30); // 30 seconds
pub async fn handle_image_sora(
app: &AppHandle,
pub async fn handle_gpt_image_1_sora(
request: EnqueueTextToImageRequest,
app: &AppHandle,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
@@ -55,10 +55,19 @@ pub async fn handle_image_sora(
Some(TextToImageSize::Wide) => ImageSize::Wide,
};
let num_images = match request.number_images {
None => NumImages::One,
Some(1) => NumImages::One,
Some(2) => NumImages::Two,
Some(3) => NumImages::Two, // NB: There is no option for three images!
Some(4) => NumImages::Four,
_ => NumImages::One,
};
let result =
simple_image_gen_with_session_auto_renew(SimpleImageGenAutoRenewRequest {
prompt: prompt.to_string(),
num_images: NumImages::One,
num_images,
image_size: size,
credentials: &creds,
request_timeout: Some(SORA_SIMPLE_IMAGE_GEN_TIMEOUT),
@@ -1 +1,3 @@
pub mod handle_image_sora;
pub (crate) mod handle_gpt_image_1_artcraft;
pub (crate) mod handle_gpt_image_1_sora;
pub (super) mod handle_gpt_image_1;
@@ -3,7 +3,7 @@ use crate::core::commands::enqueue::image::internal_image_error::InternalImageEr
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal::handle_generic_bg_removal;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::handle_gpt_image_1;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
@@ -4,8 +4,8 @@ use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal_fal::handle_generic_bg_removal_fal;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, EnqueueTextToImageSuccessResponse};
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::handle_gpt_image_1;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
@@ -208,7 +208,7 @@ pub async fn handle_request(
return Err(InternalContextualEditImageError::NoModelSpecified)
}
Some(ContextualImageEditModel::GptImage1) => {
handle_gpt_image_1(
handle_gpt_image_1_edit(
request,
app,
app_data_root,
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
@@ -16,7 +16,7 @@ use tauri::AppHandle;
pub(super) const MAX_IMAGES: usize = 10;
pub async fn handle_gpt_image_1(
pub async fn handle_gpt_image_1_edit(
request: &EnqueueContextualEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
@@ -44,8 +44,8 @@ pub async fn handle_gpt_image_1(
// the API nicer to deal with, but the user needs an additional key.
}
Provider::Artcraft => {
info!("Dispatching gpt-image-1 via Artcraft...");
return handle_gpt_image_1_artcraft(
info!("Dispatching gpt-image-1 (edit) via Artcraft...");
return handle_gpt_image_1_edit_artcraft(
request,
app,
app_data_root,
@@ -54,8 +54,8 @@ pub async fn handle_gpt_image_1(
).await;
}
Provider::Sora => {
info!("Dispatching gpt-image-1 via Sora...");
return handle_gpt_image_1_sora(
info!("Dispatching gpt-image-1 (edit) via Sora...");
return handle_gpt_image_1_edit_sora(
request,
app,
app_data_root,
@@ -2,7 +2,7 @@ use crate::core::commands::enqueue::image::enqueue_text_to_image_command::Enqueu
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::MAX_IMAGES;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::MAX_IMAGES;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
@@ -36,7 +36,7 @@ use tauri::AppHandle;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
pub async fn handle_gpt_image_1_artcraft(
pub async fn handle_gpt_image_1_edit_artcraft(
request: &EnqueueContextualEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
@@ -51,7 +51,7 @@ pub async fn handle_gpt_image_1_artcraft(
},
};
info!("Calling Artcraft gpt-image-1 ...");
info!("Calling Artcraft gpt-image-1 (edit) ...");
let uuid_idempotency_token = generate_random_uuid();
@@ -2,7 +2,7 @@ use crate::core::commands::enqueue::image::enqueue_text_to_image_command::Enqueu
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageSize, EnqueueContextualEditImageCommand, EnqueueContextualEditImageErrorType};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::MAX_IMAGES;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::MAX_IMAGES;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -48,7 +48,7 @@ const SORA_IMAGE_REMIX_TIMEOUT: Duration = Duration::from_millis(1000 * 30); //
const DEFAULT_ASPECT_RATIO : EditImageSize = EditImageSize::Square;
pub async fn handle_gpt_image_1_sora(
pub async fn handle_gpt_image_1_edit_sora(
request: &EnqueueContextualEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
@@ -1,3 +1,3 @@
pub mod handle_gpt_image_1;
pub mod handle_gpt_image_1_artcraft;
pub mod handle_gpt_image_1_sora;
pub mod handle_gpt_image_1_edit;
pub mod handle_gpt_image_1_edit_artcraft;
pub mod handle_gpt_image_1_edit_sora;
@@ -0,0 +1,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;
@@ -141,6 +141,11 @@ use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image
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;
@@ -237,6 +242,7 @@ use tokens::tokens::zs_voice_datasets::*;
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,
@@ -420,6 +426,11 @@ use tokens::tokens::zs_voice_datasets::*;
BatchGetUserRatingError,
BatchGetUserRatingQueryParams,
BatchGetUserRatingResponse,
GenerateGptImage1TextToImageImageQuality,
GenerateGptImage1TextToImageImageSize,
GenerateGptImage1TextToImageNumImages,
GenerateGptImage1TextToImageRequest,
GenerateGptImage1TextToImageResponse,
BatchInferenceJobStatusResponsePayload,
BatchMediaFileInfo,
BatchRequestDetailsResponse,
@@ -0,0 +1,216 @@
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_edit_image_webhook::enqueue_gpt_image_1_edit_image_webhook;
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::GptEditImageNumImages;
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::{GptEditImageByokArgs, GptEditImageQuality, GptEditImageSize};
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) => GptEditImageSize::Square,
Some(GenerateGptImage1TextToImageImageSize::Horizontal) => GptEditImageSize::Horizontal,
Some(GenerateGptImage1TextToImageImageSize::Vertical) => GptEditImageSize::Vertical,
None => GptEditImageSize::Square, // Default to Square
};
let num_images = match request.num_images {
Some(GenerateGptImage1TextToImageNumImages::One) => GptEditImageNumImages::One,
Some(GenerateGptImage1TextToImageNumImages::Two) => GptEditImageNumImages::Two,
Some(GenerateGptImage1TextToImageNumImages::Three) => GptEditImageNumImages::Three,
Some(GenerateGptImage1TextToImageNumImages::Four) => GptEditImageNumImages::Four,
None => GptEditImageNumImages::One, // Default to One
};
let quality = match request.image_quality {
Some(quality) => match quality {
GenerateGptImage1TextToImageImageQuality::Auto => GptEditImageQuality::Auto,
GenerateGptImage1TextToImageImageQuality::Low => GptEditImageQuality::Low,
GenerateGptImage1TextToImageImageQuality::Medium => GptEditImageQuality::Medium,
GenerateGptImage1TextToImageImageQuality::High => GptEditImageQuality::High,
},
None => GptEditImageQuality::High, // Default to High
};
let openai_api_key = OpenAiApiKey::from_str(&server_state.openai.api_key);
let args = GptEditImageByokArgs {
image_urls: vec![], // TODO(bt,2025-07-29): Don't call non-edit endpoint
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_edit_image_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_gpt_image_1_edit_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;
@@ -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()))
@@ -1,4 +1,4 @@
import { useRef } from "react";
import { useRef, useState } from "react";
import { JobContextType } from "@storyteller/common";
import { PromptBoxImage } from "@storyteller/ui-promptbox";
import BackgroundGallery from "./BackgroundGallery";
@@ -27,6 +27,13 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
const selectedModelInfo: ModelInfo | undefined =
IMAGE_MODELS_BY_LABEL[selectedModel];
const [numberImages, setNumberImages] = useState(1);
const handleNumberImagesChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const value = event.target.value;
setNumberImages(parseInt(value));
};
const jobContext: JobContextType = {
jobTokens: [],
addJobToken: () => {},
@@ -46,6 +53,14 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
<span className="pt-2 text-xl opacity-80">
Add a prompt, then generate
</span>
<select onChange={handleNumberImagesChange} value={numberImages}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</div>
<PromptBoxImage
@@ -53,6 +68,7 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
console.log("Using job context");
return jobContext;
}}
numberImages={numberImages}
model={selectedModel}
modelInfo={selectedModelInfo}
imageMediaId={imageMediaId}
@@ -8,6 +8,7 @@ import { Button, ToggleButton } from "@storyteller/ui-button";
import { Modal } from "@storyteller/ui-modal";
import {
EnqueueTextToImage,
EnqueueTextToImageRequest,
EnqueueTextToImageSize,
} from "@storyteller/tauri-api";
import {
@@ -37,6 +38,7 @@ interface PromptBoxImageProps {
model: string;
modelInfo?: ModelInfo;
imageMediaId?: string;
numberImages: number;
url?: string;
}
@@ -46,6 +48,7 @@ export const PromptBoxImage = ({
model,
modelInfo,
imageMediaId,
numberImages,
url,
}: PromptBoxImageProps) => {
useSignals();
@@ -210,11 +213,17 @@ export const PromptBoxImage = ({
const aspectRatio = getCurrentAspectRatio();
const generateResponse = await EnqueueTextToImage({
let request : EnqueueTextToImageRequest = {
prompt: prompt,
model: modelInfo,
aspect_ratio: aspectRatio,
});
};
if (numberImages >= 1 && numberImages <= 4) {
request.number_images = numberImages;
}
const generateResponse = await EnqueueTextToImage(request);
console.log("PromptBoxImage - generateResponse", generateResponse);
@@ -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,
});