diff --git a/crates/api_clients/storyteller_client/src/generate/image/generate_gpt_image_1_text_to_image.rs b/crates/api_clients/storyteller_client/src/generate/image/generate_gpt_image_1_text_to_image.rs new file mode 100644 index 0000000000..2edacc3435 --- /dev/null +++ b/crates/api_clients/storyteller_client/src/generate/image/generate_gpt_image_1_text_to_image.rs @@ -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 { + Ok(basic_json_post_request( + api_host, + GPT_IMAGE_1_TEXT_TO_IMAGE_PATH, + maybe_creds, + request, + ).await?) +} diff --git a/crates/api_clients/storyteller_client/src/generate/image/mod.rs b/crates/api_clients/storyteller_client/src/generate/image/mod.rs index 1d9185f67c..90f2d43e51 100644 --- a/crates/api_clients/storyteller_client/src/generate/image/mod.rs +++ b/crates/api_clients/storyteller_client/src/generate/image/mod.rs @@ -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; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image/enqueue_text_to_image_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image/enqueue_text_to_image_command.rs index 8398efcc75..b017fa5816 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image/enqueue_text_to_image_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image/enqueue_text_to_image_command.rs @@ -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, + + /// The number of images to generate. + pub number_images: Option, } // 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 diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/handle_gpt_image_1.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/handle_gpt_image_1.rs new file mode 100644 index 0000000000..c4ce6cb318 --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/handle_gpt_image_1.rs @@ -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 { + + 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) +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/handle_gpt_image_1_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/handle_gpt_image_1_artcraft.rs new file mode 100644 index 0000000000..3d864871cc --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/handle_gpt_image_1_artcraft.rs @@ -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 { + + 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, + }) +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/handle_image_sora.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/handle_gpt_image_1_sora.rs similarity index 92% rename from crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/handle_image_sora.rs rename to crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/handle_gpt_image_1_sora.rs index 34a206843b..6bc35a4d1b 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/handle_image_sora.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/handle_gpt_image_1_sora.rs @@ -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 { @@ -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), diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/mod.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/mod.rs index b07116581b..692ac03133 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/mod.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image/gpt_image_1/mod.rs @@ -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; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/enqueue_image_bg_removal_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/enqueue_image_bg_removal_command.rs index 9c53b4b806..8e1d630e79 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/enqueue_image_bg_removal_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/enqueue_image_bg_removal_command.rs @@ -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}; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/generic/handle_generic_bg_removal.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/generic/handle_generic_bg_removal.rs index 75eeacc3db..620b19b451 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/generic/handle_generic_bg_removal.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/generic/handle_generic_bg_removal.rs @@ -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; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_contextual_edit_image_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_contextual_edit_image_command.rs index 75a4b00acb..c579f1c7e7 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_contextual_edit_image_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_contextual_edit_image_command.rs @@ -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, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_edit.rs similarity index 87% rename from crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1.rs rename to crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_edit.rs index cb63e2fc3a..45a7f0a344 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_edit.rs @@ -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, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_edit_artcraft.rs similarity index 98% rename from crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_artcraft.rs rename to crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_edit_artcraft.rs index 6250f2be66..1559a38131 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_artcraft.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_edit_artcraft.rs @@ -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(); diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_sora.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_edit_sora.rs similarity index 99% rename from crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_sora.rs rename to crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_edit_sora.rs index ba3c6af003..6d3a4d3166 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_sora.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_edit_sora.rs @@ -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, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/mod.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/mod.rs index 73b5e0a985..0aaff118a9 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/mod.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/mod.rs @@ -1,3 +1,3 @@ -pub mod handle_gpt_image_1; -pub mod handle_gpt_image_1_artcraft; -pub mod handle_gpt_image_1_sora; \ No newline at end of file +pub mod handle_gpt_image_1_edit; +pub mod handle_gpt_image_1_edit_artcraft; +pub mod handle_gpt_image_1_edit_sora; \ No newline at end of file diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_gpt_image_1_text_to_image.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_gpt_image_1_text_to_image.rs new file mode 100644 index 0000000000..6425b5760a --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/image/generate_gpt_image_1_text_to_image.rs @@ -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, + + /// Size of the image to generate. Default is Square. + pub image_size: Option, + + /// Number of images to generate. Default is one. + pub num_images: Option, + + /// Quality of the image to generate. Default is High. + pub image_quality: Option, +} + + +#[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, +} diff --git a/crates/schema/public/api/artcraft_api_defs/src/generate/image/mod.rs b/crates/schema/public/api/artcraft_api_defs/src/generate/image/mod.rs index 1d9185f67c..90f2d43e51 100644 --- a/crates/schema/public/api/artcraft_api_defs/src/generate/image/mod.rs +++ b/crates/schema/public/api/artcraft_api_defs/src/generate/image/mod.rs @@ -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; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index 830e16d075..ac7d821f17 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -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, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_gpt_image_1_text_to_image_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_gpt_image_1_text_to_image_handler.rs new file mode 100644 index 0000000000..87d4d20a63 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/generate_gpt_image_1_text_to_image_handler.rs @@ -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, + server_state: web::Data> +) -> Result, 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, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/mod.rs index ebc12c1b8b..378ead72b8 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/image/mod.rs @@ -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; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/routes/add_generate_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/add_generate_routes.rs index 6466b065cd..8700b54ff8 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/add_generate_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/add_generate_routes.rs @@ -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 (app: App) -> App 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())) diff --git a/frontend/apps/editor3d/app/src/pages/PageImage/TextToImage.tsx b/frontend/apps/editor3d/app/src/pages/PageImage/TextToImage.tsx index 45f1a4eb4a..8103a86490 100644 --- a/frontend/apps/editor3d/app/src/pages/PageImage/TextToImage.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageImage/TextToImage.tsx @@ -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) => { + const value = event.target.value; + setNumberImages(parseInt(value)); + }; + const jobContext: JobContextType = { jobTokens: [], addJobToken: () => {}, @@ -46,6 +53,14 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => { Add a prompt, then generate + + + { console.log("Using job context"); return jobContext; }} + numberImages={numberImages} model={selectedModel} modelInfo={selectedModelInfo} imageMediaId={imageMediaId} diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx index fceec50c5f..90474fd0c8 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx @@ -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); diff --git a/frontend/libs/tauri-api/src/lib/enqueue/EnqueueTextToImage.ts b/frontend/libs/tauri-api/src/lib/enqueue/EnqueueTextToImage.ts index 397d100841..30ed43f9ff 100644 --- a/frontend/libs/tauri-api/src/lib/enqueue/EnqueueTextToImage.ts +++ b/frontend/libs/tauri-api/src/lib/enqueue/EnqueueTextToImage.ts @@ -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, });