Compare commits

...
Author SHA1 Message Date
Brandon Thomas 62947529b1 generation counts 2025-12-06 04:46:13 -05:00
Brandon Thomas 3b5a2d5873 fixes 2025-12-06 04:32:52 -05:00
Brandon Thomas 6e8d2a6f58 nano banana text-to-image 2025-12-06 04:29:36 -05:00
Brandon Thomas 518438248f nano bannana pro t2i wip 2025-12-06 04:24:25 -05:00
Brandon Thomas 5d7835a629 refactor 2025-12-06 04:21:46 -05:00
Brandon Thomas a632682fe9 refactor/rename command 2025-12-06 04:17:28 -05:00
Brandon Thomas 7b9854431a temporary hack to send image editing to "inpaint editing" UX 2025-12-06 03:04:48 -05:00
Brandon Thomas 3e6323e91b fixup inpaint models 2025-12-06 02:27:01 -05:00
Brandon Thomas 7edbd788df frontend changes wip 2025-12-06 02:18:59 -05:00
Brandon Thomas dbe492f197 adding nanobanana pro to tauri 2025-12-06 01:09:33 -05:00
61 changed files with 719 additions and 145 deletions
@@ -1,4 +1,4 @@
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageSuccessResponse;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageSuccessResponse;
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
@@ -2,7 +2,7 @@ use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal_artcraft::handle_generic_bg_removal_artcraft;
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::enqueue_edit_image_command::EnqueueEditImageCommand;
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;
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
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, GenerationServiceProvider};
@@ -1,8 +1,9 @@
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::flux_kontext::handle_flux_kontext_edit::handle_flux_kontext_edit;
use crate::core::commands::enqueue::image_edit::gemini_25_flash::handle_gemini_25_flash_edit::handle_gemini_25_flash_edit;
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::image_edit::nano_banana::handle_nano_banana_edit::handle_nano_banana_edit;
use crate::core::commands::enqueue::image_edit::nano_banana_pro::handle_nano_banana_pro_edit::handle_nano_banana_pro_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};
@@ -36,8 +37,8 @@ use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use serde_derive::{Deserialize, Serialize};
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
use std::time::Duration;
use storyteller_client::error::storyteller_error::StorytellerError;
use storyteller_client::endpoints::media_files::get_media_file::get_media_file;
use storyteller_client::error::storyteller_error::StorytellerError;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
@@ -46,13 +47,19 @@ use tokens::tokens::media_files::MediaFileToken;
/// Don't change the serializations without coordinating with the frontend.
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum ContextualImageEditModel {
pub enum ImageEditModel {
#[serde(rename = "flux_pro_kontext_max")]
FluxProKontextMax,
#[serde(rename = "gemini_25_flash")]
Gemini25Flash,
#[serde(rename = "nano_banana")]
NanoBanana,
#[serde(rename = "nano_banana_pro")]
NanoBananaPro,
#[serde(rename = "gpt_image_1")]
GptImage1,
@@ -64,9 +71,9 @@ pub enum ContextualImageEditModel {
}
#[derive(Deserialize, Debug)]
pub struct EnqueueContextualEditImageCommand {
pub struct EnqueueEditImageCommand {
/// The model to use.
pub model: Option<ContextualImageEditModel>,
pub model: Option<ImageEditModel>,
/// Images to use for the image edit.
/// The first image is typically a 2D canvas or 3D stage, but doesn't have to be.
@@ -93,6 +100,9 @@ pub struct EnqueueContextualEditImageCommand {
/// Image quality.
pub image_quality: Option<EditImageQuality>,
/// Image resolution
pub image_resolution: Option<EditImageResolution>,
/// OPTIONAL.
/// Name of the frontend caller.
/// We'll use this to selectively trigger events.
@@ -127,9 +137,20 @@ pub enum EditImageQuality {
Low,
}
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EditImageResolution {
/// 1K - nano banana pro
OneK,
/// 2K - nano banana pro
TwoK,
/// 4K - nano banana pro
FourK,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueContextualEditImageErrorType {
pub enum EnqueueEditImageErrorType {
/// Caller didn't specify a model
ModelNotSpecified,
@@ -153,15 +174,15 @@ pub enum EnqueueContextualEditImageErrorType {
}
#[derive(Serialize)]
pub struct EnqueueContextualEditImageSuccessResponse {
pub struct EnqueueEditImageSuccessResponse {
}
impl SerializeMarker for EnqueueContextualEditImageSuccessResponse {}
impl SerializeMarker for EnqueueEditImageSuccessResponse {}
#[tauri::command]
pub async fn enqueue_contextual_edit_image_command(
pub async fn enqueue_edit_image_command(
app: AppHandle,
request: EnqueueContextualEditImageCommand,
request: EnqueueEditImageCommand,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
provider_priority_store: State<'_, ProviderPriorityStore>,
@@ -169,9 +190,9 @@ pub async fn enqueue_contextual_edit_image_command(
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
sora_creds_manager: State<'_, SoraCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
) -> ResponseOrErrorType<EnqueueContextualEditImageSuccessResponse, EnqueueContextualEditImageErrorType> {
) -> ResponseOrErrorType<EnqueueEditImageSuccessResponse, EnqueueEditImageErrorType> {
info!("enqueue_contextual_edit_image_command called; image media tokens : {:?}, full request: {:?}",
info!("enqueue_edit_image_command called; image media tokens : {:?}, full request: {:?}",
&request.image_media_tokens, &request);
let result = handle_request(
@@ -219,13 +240,13 @@ pub async fn enqueue_contextual_edit_image_command(
CreditsBalanceChangedEvent{}.send_infallible(&app);
Ok(EnqueueContextualEditImageSuccessResponse {}.into())
Ok(EnqueueEditImageSuccessResponse {}.into())
}
}
}
pub async fn handle_request(
request: &EnqueueContextualEditImageCommand,
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
@@ -235,11 +256,11 @@ pub async fn handle_request(
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let success_event= match request.model {
let mut success_event= match request.model {
None => {
return Err(GenerateError::no_model_specified())
}
Some(ContextualImageEditModel::FluxProKontextMax) => {
Some(ImageEditModel::FluxProKontextMax) => {
handle_flux_kontext_edit(
request,
app,
@@ -251,8 +272,8 @@ pub async fn handle_request(
sora_task_queue,
).await?
}
Some(ContextualImageEditModel::Gemini25Flash) => {
handle_gemini_25_flash_edit(
Some(ImageEditModel::Gemini25Flash | ImageEditModel::NanoBanana) => {
handle_nano_banana_edit(
request,
app,
app_data_root,
@@ -263,7 +284,19 @@ pub async fn handle_request(
sora_task_queue,
).await?
}
Some(ContextualImageEditModel::GptImage1) => {
Some(ImageEditModel::NanoBananaPro) => {
handle_nano_banana_pro_edit(
request,
app,
app_data_root,
app_env_configs,
provider_priority_store,
storyteller_creds_manager,
sora_creds_manager,
sora_task_queue,
).await?
}
Some(ImageEditModel::GptImage1) => {
handle_gpt_image_1_edit(
request,
app,
@@ -276,7 +309,18 @@ pub async fn handle_request(
).await?
}
};
match request.frontend_caller {
Some(TauriCommandCaller::ImageEditor) => {
// NB: Certain image generations enqueued in the inpainting editor are classed
// as "image_generation" and won't update the inpainting UI. This is a hack to
// temporarily fix the UI event binding.
// TODO: The frontend shouldn't require us to change the type to inpainting.
success_event.task_type = TaskType::ImageInpaintEdit;
}
_ => {} // fall-through
}
let result = success_event
.insert_into_task_database_with_frontend_payload(
task_database,
@@ -294,31 +338,31 @@ pub async fn handle_request(
Ok(success_event)
}
fn error_to_tauri_response(error: GenerateError) -> CommandErrorResponseWrapper<EnqueueContextualEditImageErrorType, ()> {
fn error_to_tauri_response(error: GenerateError) -> CommandErrorResponseWrapper<EnqueueEditImageErrorType, ()> {
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueContextualEditImageErrorType::ServerError;
let mut error_type = EnqueueEditImageErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.".to_string();
match error {
GenerateError::BadInput(BadInputReason::NoModelSpecified) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueContextualEditImageErrorType::ModelNotSpecified;
error_type = EnqueueEditImageErrorType::ModelNotSpecified;
error_message = "No model specified for image generation".to_string();
}
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueContextualEditImageErrorType::NoProviderAvailable;
error_type = EnqueueEditImageErrorType::NoProviderAvailable;
error_message = "No configured provider available for image generation".to_string();
}
GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages { min, max, requested }) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueContextualEditImageErrorType::BadRequest;
error_type = EnqueueEditImageErrorType::BadRequest;
error_message = format!("Invalid number of images requested ({}). Must be between {} and {}", requested, min, max);
}
GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages{ min, max, provided }) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueContextualEditImageErrorType::BadRequest;
error_type = EnqueueEditImageErrorType::BadRequest;
error_message = format!("Invalid number of input images ({}). Must be between {} and {}", provided, min, max);
}
_ => {} // Fall-through for now
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
use crate::core::commands::enqueue::image_edit::flux_kontext::handle_flux_kontext_edit_artcraft::handle_flux_kontext_edit_artcraft;
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;
@@ -14,7 +14,7 @@ use log::info;
use tauri::AppHandle;
pub async fn handle_flux_kontext_edit(
request: &EnqueueContextualEditImageCommand,
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
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};
@@ -30,7 +30,7 @@ use storyteller_client::endpoints::generate::image::generate_flux_pro_11_ultra_t
use tauri::AppHandle;
pub async fn handle_flux_kontext_edit_artcraft(
request: &EnqueueContextualEditImageCommand,
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
@@ -1,2 +0,0 @@
pub (super) mod handle_gemini_25_flash_edit;
pub (super) mod handle_gemini_25_flash_edit_artcraft;
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
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;
@@ -15,7 +15,7 @@ use tauri::AppHandle;
pub(super) const MAX_IMAGES: usize = 10;
pub async fn handle_gpt_image_1_edit(
request: &EnqueueContextualEditImageCommand,
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
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;
@@ -29,7 +29,7 @@ use storyteller_client::endpoints::generate::image::generate_flux_pro_11_ultra_t
use tauri::AppHandle;
pub async fn handle_gpt_image_1_edit_artcraft(
request: &EnqueueContextualEditImageCommand,
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageSize, EnqueueContextualEditImageCommand, EnqueueContextualEditImageErrorType};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageSize, EnqueueEditImageCommand, EnqueueEditImageErrorType};
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};
@@ -42,7 +42,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_edit_sora(
request: &EnqueueContextualEditImageCommand,
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
@@ -1,4 +1,5 @@
pub mod enqueue_contextual_edit_image_command;
pub mod enqueue_edit_image_command;
pub mod gpt_image_1;
mod flux_kontext;
mod gemini_25_flash;
mod nano_banana;
mod nano_banana_pro;
@@ -1,6 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::gemini_25_flash::handle_gemini_25_flash_edit_artcraft::handle_gemini_25_flash_edit_artcraft;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
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;
@@ -12,9 +11,10 @@ use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use log::info;
use tauri::AppHandle;
use crate::core::commands::enqueue::image_edit::nano_banana::handle_nano_banana_edit_artcraft::handle_nano_banana_edit_artcraft;
pub async fn handle_gemini_25_flash_edit(
request: &EnqueueContextualEditImageCommand,
pub async fn handle_nano_banana_edit(
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
@@ -39,8 +39,8 @@ pub async fn handle_gemini_25_flash_edit(
// Fallthrough
}
Provider::Artcraft => {
info!("Dispatching gemini 2.5 flash (edit) via Artcraft...");
return handle_gemini_25_flash_edit_artcraft(
info!("Dispatching Nano Banana (edit) via Artcraft...");
return handle_nano_banana_edit_artcraft(
request,
app,
app_data_root,
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
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};
@@ -29,8 +29,8 @@ use tauri::AppHandle;
pub(super) const MAX_IMAGES: usize = 10;
pub async fn handle_gemini_25_flash_edit_artcraft(
request: &EnqueueContextualEditImageCommand,
pub async fn handle_nano_banana_edit_artcraft(
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
@@ -44,7 +44,7 @@ pub async fn handle_gemini_25_flash_edit_artcraft(
},
};
info!("Calling Artcraft gemini 2.5 flash (edit) ...");
info!("Calling Artcraft Nano Banana (edit) ...");
let uuid_idempotency_token = generate_random_uuid();
@@ -98,12 +98,12 @@ pub async fn handle_gemini_25_flash_edit_artcraft(
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft gpt-image-1. Job token: {}",
info!("Successfully enqueued Artcraft Nano Banana. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
error!("Failed to use Artcraft Nano Banana: {:?}", err);
return Err(GenerateError::from(err));
}
};
@@ -0,0 +1,2 @@
pub (super) mod handle_nano_banana_edit;
pub (super) mod handle_nano_banana_edit_artcraft;
@@ -0,0 +1,53 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
use crate::core::commands::enqueue::image_edit::nano_banana_pro::handle_nano_banana_pro_edit_artcraft::handle_nano_banana_pro_edit_artcraft;
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;
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 log::info;
use tauri::AppHandle;
pub async fn handle_nano_banana_pro_edit(
request: &EnqueueEditImageCommand,
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, GenerateError> {
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 (TODO)
}
Provider::Sora => {
// Fallthrough
}
Provider::Artcraft => {
info!("Dispatching Nano Banana Pro (edit) via Artcraft...");
return handle_nano_banana_pro_edit_artcraft(
request,
app,
app_data_root,
app_env_configs,
storyteller_creds_manager
).await;
}
}
}
Err(GenerateError::NoProviderAvailable)
}
@@ -0,0 +1,137 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageResolution, EditImageSize, EnqueueEditImageCommand};
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::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 anyhow::anyhow;
use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{FluxProKontextMaxEditImageNumImages, FluxProKontextMaxEditImageRequest};
use artcraft_api_defs::generate::image::edit::gemini_25_flash_edit_image::{Gemini25FlashEditImageNumImages, Gemini25FlashEditImageRequest};
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use storyteller_client::endpoints::generate::image::edit::flux_pro_kontext_max_edit_image::flux_pro_kontext_max_edit_image;
use storyteller_client::endpoints::generate::image::edit::gemini_25_flash_edit_image::gemini_25_flash_edit_image;
use storyteller_client::endpoints::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
use tauri::AppHandle;
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{NanoBananaProMultiFunctionImageGenAspectRatio, NanoBananaProMultiFunctionImageGenImageResolution, NanoBananaProMultiFunctionImageGenNumImages, NanoBananaProMultiFunctionImageGenRequest};
use storyteller_client::endpoints::generate::image::multi_function::nano_banana_pro_multi_function_image_gen_image::nano_banana_pro_multi_function_image_gen;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::ImageEditModel::NanoBanana;
pub(super) const MAX_IMAGES: usize = 10;
pub async fn handle_nano_banana_pro_edit_artcraft(
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(GenerateError::needs_storyteller_credentials());
},
};
info!("Calling Artcraft Nano Banana (edit) ...");
let uuid_idempotency_token = generate_random_uuid();
let num_images = match request.image_count {
None => None,
Some(1) => Some(NanoBananaProMultiFunctionImageGenNumImages::One),
Some(2) => Some(NanoBananaProMultiFunctionImageGenNumImages::Two),
Some(3) => Some(NanoBananaProMultiFunctionImageGenNumImages::Three),
Some(4) => Some(NanoBananaProMultiFunctionImageGenNumImages::Four),
Some(other) => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
}));
},
};
let aspect_ratio = request.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EditImageSize::Auto => None,
EditImageSize::Square => Some(NanoBananaProMultiFunctionImageGenAspectRatio::OneByOne),
EditImageSize::Wide => Some(NanoBananaProMultiFunctionImageGenAspectRatio::SixteenByNine),
EditImageSize::Tall => Some(NanoBananaProMultiFunctionImageGenAspectRatio::NineBySixteen),
})
.flatten();
let resolution = request.image_resolution
.map(|image_resolution| match image_resolution {
EditImageResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
EditImageResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK,
EditImageResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
});
let mut media_tokens = Vec::with_capacity(10);
if let Some(scene_image_media_token) = request.scene_image_media_token.clone() {
media_tokens.push(scene_image_media_token);
}
if let Some(image_media_tokens) = request.image_media_tokens.as_ref() {
media_tokens.extend_from_slice(image_media_tokens);
}
if media_tokens.len() > MAX_IMAGES {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages {
min: 1,
max: MAX_IMAGES as u32,
provided: media_tokens.len() as u32,
}));
}
let request = NanoBananaProMultiFunctionImageGenRequest {
uuid_idempotency_token,
prompt: Some(request.prompt.clone()),
image_media_tokens: Some(media_tokens),
num_images,
resolution,
aspect_ratio,
};
let result = nano_banana_pro_multi_function_image_gen(
&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 Nano Banana. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Nano Banana: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::NanoBananaPro),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
})
}
@@ -0,0 +1,2 @@
pub (super) mod handle_nano_banana_pro_edit;
pub (super) mod handle_nano_banana_pro_edit_artcraft;
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
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::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
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::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::image_inpaint::flux_pro_kontext_inpaint::handle_flux_pro_kontext_inpaint_artcraft::handle_flux_pro_kontext_inpaint_artcraft;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::image_inpaint::flux_pro_kontext_inpaint::handle_flux_pro_kontext_inpaint_artcraft::handle_flux_pro_kontext_inpaint_artcraft;
use crate::core::commands::enqueue::image_inpaint::gemini_25_flash_inpaint::handle_gemini_25_flash_inpaint_artcraft::handle_gemini_25_flash_inpaint_artcraft;
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, GrokAspectRatio, SoraOrientation};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
use crate::core::commands::enqueue::image_to_video::sora2::handle_sora2_video_sora::handle_sora2_video_sora;
@@ -66,6 +66,8 @@ impl TaskEnqueueSuccess{
Some(GenerationModel::FluxPro11Ultra) => Some(TaskModelType::FluxPro11Ultra),
Some(GenerationModel::FluxProKontextMax) => Some(TaskModelType::FluxProKontextMax),
Some(GenerationModel::Gemini25Flash) => Some(TaskModelType::Gemini25Flash),
Some(GenerationModel::NanoBanana) => Some(TaskModelType::NanoBanana),
Some(GenerationModel::NanoBananaPro) => Some(TaskModelType::NanoBananaPro),
Some(GenerationModel::GptImage1) => Some(TaskModelType::GptImage1),
Some(GenerationModel::GrokImage) => Some(TaskModelType::GrokImage),
Some(GenerationModel::Recraft3) => Some(TaskModelType::Recraft3),
@@ -1,13 +1,14 @@
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::gemini_25_flash::handle_gemini_25_flash::handle_gemini_25_flash;
use crate::core::commands::enqueue::text_to_image::generic::handle_image_artcraft::handle_image_artcraft;
use crate::core::commands::enqueue::text_to_image::generic::handle_image_fal::handle_image_fal;
use crate::core::commands::enqueue::text_to_image::gpt_image_1::handle_gpt_image_1::handle_gpt_image_1;
use crate::core::commands::enqueue::text_to_image::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::commands::enqueue::text_to_image::grok::handle_grok::handle_grok;
use crate::core::commands::enqueue::text_to_image::midjourney::handle_midjourney::handle_midjourney;
use crate::core::commands::enqueue::text_to_image::nano_banana::handle_nano_banana::handle_nano_banana;
use crate::core::commands::enqueue::text_to_image::nano_banana_pro::handle_nano_banana_pro::handle_nano_banana_pro;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
@@ -58,6 +59,10 @@ pub enum ImageModel {
#[serde(rename = "gemini_25_flash")]
Gemini25Flash,
#[serde(rename = "nano_banana")]
NanoBanana,
#[serde(rename = "nano_banana_pro")]
NanoBananaPro,
// Generic Midjourney model, version unknown.
#[serde(rename = "midjourney")]
@@ -75,6 +80,9 @@ pub struct EnqueueTextToImageRequest {
/// Aspect ratio.
pub aspect_ratio: Option<TextToImageSize>,
/// Aspect ratio.
pub image_resolution: Option<TextToImageResolution>,
/// The number of images to generate.
pub number_images: Option<u32>,
@@ -116,6 +124,17 @@ pub enum TextToImageSize {
Tall,
}
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum TextToImageResolution {
/// 1K - nano banana pro
OneK,
/// 2K - nano banana pro
TwoK,
/// 4K - nano banana pro
FourK,
}
#[derive(Serialize)]
pub struct EnqueueTextToImageSuccessResponse {
}
@@ -297,8 +316,20 @@ pub async fn dispatch_request(
None => {
return Err(GenerateError::BadInput(BadInputReason::NoModelSpecified));
}
Some(ImageModel::Gemini25Flash) => {
return handle_gemini_25_flash(
Some(ImageModel::NanoBanana | ImageModel::Gemini25Flash) => {
return handle_nano_banana(
request,
app,
app_data_root,
app_env_configs,
provider_priority_store,
storyteller_creds_manager,
sora_creds_manager,
sora_task_queue,
).await;
}
Some(ImageModel::NanoBananaPro) => {
return handle_nano_banana_pro(
request,
app,
app_data_root,
@@ -1,2 +0,0 @@
pub (crate) mod handle_gemini_25_flash_artcraft;
pub (super) mod handle_gemini_25_flash;
@@ -57,6 +57,8 @@ pub async fn handle_image_artcraft(
}
Some(
ImageModel::Gemini25Flash |
ImageModel::NanoBanana |
ImageModel::NanoBananaPro |
ImageModel::GptImage1 |
ImageModel::GrokImage |
ImageModel::Midjourney
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -1,6 +1,7 @@
pub mod enqueue_text_to_image_command;
pub mod generic;
mod gemini_25_flash;
mod nano_banana;
mod nano_banana_pro;
mod gpt_image_1;
mod midjourney;
mod grok;
@@ -3,9 +3,9 @@ use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_
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::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::text_to_image::gemini_25_flash::handle_gemini_25_flash_artcraft::handle_gemini_25_flash_artcraft;
use crate::core::commands::enqueue::text_to_image::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
use crate::core::commands::enqueue::text_to_image::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::commands::enqueue::text_to_image::nano_banana::handle_nano_banana_artcraft::handle_nano_banana_artcraft;
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;
@@ -24,7 +24,7 @@ use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use std::time::Duration;
use tauri::AppHandle;
pub async fn handle_gemini_25_flash(
pub async fn handle_nano_banana(
request: &EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
@@ -50,8 +50,8 @@ pub async fn handle_gemini_25_flash(
// Fallthrough
}
Provider::Artcraft => {
info!("Dispatching gemini 2.5 flash via Artcraft...");
return handle_gemini_25_flash_artcraft(
info!("Dispatching Nano Banana via Artcraft...");
return handle_nano_banana_artcraft(
request,
app_env_configs,
storyteller_creds_manager
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -26,7 +26,7 @@ use storyteller_client::endpoints::generate::image::edit::gpt_image_1_edit_image
use storyteller_client::endpoints::generate::image::generate_gpt_image_1_text_to_image::generate_gpt_image_1_text_to_image;
use tauri::AppHandle;
pub async fn handle_gemini_25_flash_artcraft(
pub async fn handle_nano_banana_artcraft(
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -39,7 +39,7 @@ pub async fn handle_gemini_25_flash_artcraft(
},
};
info!("Calling Artcraft gemini 2.5 flash ...");
info!("Calling Artcraft Nano Banana...");
let uuid_idempotency_token = generate_random_uuid();
@@ -69,12 +69,12 @@ pub async fn handle_gemini_25_flash_artcraft(
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft gemini 2.5 flash. Job token: {}",
info!("Successfully enqueued Artcraft Nano Banana. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft gemini 2.5 flash: {:?}", err);
error!("Failed to use Artcraft Nano Banana: {:?}", err);
return Err(GenerateError::ProviderFailure(ProviderFailureReason::StorytellerError(err)));
}
};
@@ -0,0 +1,2 @@
pub (crate) mod handle_nano_banana_artcraft;
pub (super) mod handle_nano_banana;
@@ -0,0 +1,59 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::text_to_image::nano_banana_pro::handle_nano_banana_pro_artcraft::handle_nano_banana_pro_artcraft;
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::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 log::{error, info};
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_nano_banana_pro(
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, GenerateError> {
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 (for now)
}
Provider::Sora => {
// Fallthrough
}
Provider::Artcraft => {
info!("Dispatching Nano Banana Pro via Artcraft...");
return handle_nano_banana_pro_artcraft(
request,
app_env_configs,
storyteller_creds_manager
).await;
}
}
}
Err(GenerateError::NoProviderAvailable)
}
@@ -0,0 +1,111 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageResolution, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageResolution, TextToImageSize};
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::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::edit::gemini_25_flash_edit_image::{Gemini25FlashEditImageNumImages, Gemini25FlashEditImageRequest};
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest};
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{NanoBananaProMultiFunctionImageGenAspectRatio, NanoBananaProMultiFunctionImageGenImageResolution, NanoBananaProMultiFunctionImageGenNumImages, NanoBananaProMultiFunctionImageGenRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
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::endpoints::generate::image::edit::gemini_25_flash_edit_image::gemini_25_flash_edit_image;
use storyteller_client::endpoints::generate::image::multi_function::nano_banana_pro_multi_function_image_gen_image::nano_banana_pro_multi_function_image_gen;
use tauri::AppHandle;
pub async fn handle_nano_banana_pro_artcraft(
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials));
},
};
info!("Calling Artcraft Nano Banana Pro...");
let uuid_idempotency_token = generate_random_uuid();
let num_images = match request.number_images {
None => None,
Some(1) => Some(NanoBananaProMultiFunctionImageGenNumImages::One),
Some(2) => Some(NanoBananaProMultiFunctionImageGenNumImages::Two),
Some(3) => Some(NanoBananaProMultiFunctionImageGenNumImages::Three),
Some(4) => Some(NanoBananaProMultiFunctionImageGenNumImages::Four),
Some(other) => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
}));
},
};
let aspect_ratio = request.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
TextToImageSize::Auto => None,
TextToImageSize::Square => Some(NanoBananaProMultiFunctionImageGenAspectRatio::OneByOne),
TextToImageSize::Wide => Some(NanoBananaProMultiFunctionImageGenAspectRatio::SixteenByNine),
TextToImageSize::Tall => Some(NanoBananaProMultiFunctionImageGenAspectRatio::NineBySixteen),
})
.flatten();
let resolution = request.image_resolution
.map(|image_resolution| match image_resolution {
TextToImageResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
TextToImageResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK,
TextToImageResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
});
let request = NanoBananaProMultiFunctionImageGenRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
image_media_tokens: None,
num_images,
resolution,
aspect_ratio,
};
let result = nano_banana_pro_multi_function_image_gen(
&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 Nano Banana. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Nano Banana: {:?}", err);
return Err(GenerateError::ProviderFailure(ProviderFailureReason::StorytellerError(err)));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::NanoBananaPro),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
})
}
@@ -0,0 +1,2 @@
pub (crate) mod handle_nano_banana_pro_artcraft;
pub (super) mod handle_nano_banana_pro;
@@ -34,7 +34,11 @@ pub enum GenerationModel {
GrokImage,
#[serde(rename = "gemini_25_flash")]
Gemini25Flash,
#[serde(rename = "nano_banana")]
NanoBanana,
#[serde(rename = "nano_banana_pro")]
NanoBananaPro,
#[serde(rename = "hunyuan_3d_2_0")]
Hunyuan3d2_0,
#[serde(rename = "hunyuan_3d_2_1")]
+2 -2
View File
@@ -6,7 +6,7 @@ use tauri::Manager;
use crate::core::commands::app_preferences::get_app_preferences_command::get_app_preferences_command;
use crate::core::commands::app_preferences::update_app_preference_command::update_app_preferences_command;
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::enqueue_image_bg_removal_command;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::enqueue_contextual_edit_image_command;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::enqueue_edit_image_command;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::enqueue_image_inpaint_command;
use crate::core::commands::enqueue::image_to_object::enqueue_image_to_3d_object_command::enqueue_image_to_3d_object_command;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::enqueue_image_to_video_command;
@@ -172,7 +172,7 @@ pub fn run() {
// My first attempt at naively doing this didn't work because the macros can't find their codegen'd targets.
let builder = builder.invoke_handler(tauri::generate_handler![
check_sora_session_command,
enqueue_contextual_edit_image_command,
enqueue_edit_image_command,
enqueue_image_bg_removal_command,
enqueue_image_inpaint_command,
enqueue_image_to_3d_object_command,
@@ -1,4 +1,4 @@
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{ContextualImageEditModel, EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{ImageEditModel, EditImageQuality, EditImageSize};
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
@@ -1,5 +1,5 @@
use chrono::{DateTime, Utc};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{ContextualImageEditModel, EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{ImageEditModel, EditImageQuality, EditImageSize};
use crate::core::commands::providers::get_provider_order_command::GetProviderOrderResponse;
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
@@ -1,4 +1,4 @@
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageSuccessResponse;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageSuccessResponse;
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
@@ -1,4 +1,4 @@
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{ContextualImageEditModel, EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{ImageEditModel, EditImageQuality, EditImageSize};
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
@@ -1,4 +1,4 @@
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{ContextualImageEditModel, EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{ImageEditModel, EditImageQuality, EditImageSize};
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
@@ -1,4 +1,4 @@
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{ContextualImageEditModel, EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{ImageEditModel, EditImageQuality, EditImageSize};
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
@@ -1,4 +1,4 @@
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{ContextualImageEditModel, EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{ImageEditModel, EditImageQuality, EditImageSize};
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
@@ -1,4 +1,4 @@
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{ContextualImageEditModel, EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{ImageEditModel, EditImageQuality, EditImageSize};
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
@@ -1,4 +1,4 @@
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{ContextualImageEditModel, EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{ImageEditModel, EditImageQuality, EditImageSize};
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
@@ -29,6 +29,10 @@ pub enum TaskModelType {
FluxProKontextMax,
#[serde(rename = "gemini_25_flash")]
Gemini25Flash,
#[serde(rename = "nano_banana")]
NanoBanana,
#[serde(rename = "nano_banana_pro")]
NanoBananaPro,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "grok_image")]
@@ -85,6 +89,8 @@ impl TaskModelType {
Self::FluxPro11Ultra => "flux_pro_1.1_ultra",
Self::FluxProKontextMax => "flux_pro_kontext_max",
Self::Gemini25Flash => "gemini_25_flash",
Self::NanoBanana => "nano_banana",
Self::NanoBananaPro => "nano_banana_pro",
Self::GptImage1 => "gpt_image_1",
Self::GrokImage => "grok_image",
Self::Recraft3 => "recraft_3",
@@ -116,6 +122,8 @@ impl TaskModelType {
"flux_pro_1.1_ultra" => Ok(Self::FluxPro11Ultra),
"flux_pro_kontext_max" => Ok(Self::FluxProKontextMax),
"gemini_25_flash" => Ok(Self::Gemini25Flash),
"nano_banana" => Ok(Self::NanoBanana),
"nano_banana_pro" => Ok(Self::NanoBananaPro),
"gpt_image_1" => Ok(Self::GptImage1),
"grok_image" => Ok(Self::GrokImage),
"recraft_3" => Ok(Self::Recraft3),
@@ -150,6 +158,8 @@ impl TaskModelType {
Self::FluxPro11Ultra,
Self::FluxProKontextMax,
Self::Gemini25Flash,
Self::NanoBanana,
Self::NanoBananaPro,
Self::GptImage1,
Self::GrokImage,
Self::Recraft3,
@@ -191,6 +201,8 @@ mod tests {
assert_serialization(TaskModelType::FluxPro11Ultra, "flux_pro_1.1_ultra");
assert_serialization(TaskModelType::FluxProKontextMax, "flux_pro_kontext_max");
assert_serialization(TaskModelType::Gemini25Flash, "gemini_25_flash");
assert_serialization(TaskModelType::NanoBanana, "nano_banana");
assert_serialization(TaskModelType::NanoBananaPro, "nano_banana_pro");
assert_serialization(TaskModelType::GptImage1, "gpt_image_1");
assert_serialization(TaskModelType::GrokImage, "grok_image");
assert_serialization(TaskModelType::Recraft3, "recraft_3");
@@ -221,6 +233,8 @@ mod tests {
assert_eq!(TaskModelType::FluxPro11Ultra.to_str(), "flux_pro_1.1_ultra");
assert_eq!(TaskModelType::FluxProKontextMax.to_str(), "flux_pro_kontext_max");
assert_eq!(TaskModelType::Gemini25Flash.to_str(), "gemini_25_flash");
assert_eq!(TaskModelType::NanoBanana.to_str(), "nano_banana");
assert_eq!(TaskModelType::NanoBananaPro.to_str(), "nano_banana_pro");
assert_eq!(TaskModelType::GptImage1.to_str(), "gpt_image_1");
assert_eq!(TaskModelType::GrokImage.to_str(), "grok_image");
assert_eq!(TaskModelType::Recraft3.to_str(), "recraft_3");
@@ -251,6 +265,8 @@ mod tests {
assert_eq!(TaskModelType::from_str("flux_pro_1.1_ultra").unwrap(), TaskModelType::FluxPro11Ultra);
assert_eq!(TaskModelType::from_str("flux_pro_kontext_max").unwrap(), TaskModelType::FluxProKontextMax);
assert_eq!(TaskModelType::from_str("gemini_25_flash").unwrap(), TaskModelType::Gemini25Flash);
assert_eq!(TaskModelType::from_str("nano_banana").unwrap(), TaskModelType::NanoBanana);
assert_eq!(TaskModelType::from_str("nano_banana_pro").unwrap(), TaskModelType::NanoBananaPro);
assert_eq!(TaskModelType::from_str("gpt_image_1").unwrap(), TaskModelType::GptImage1);
assert_eq!(TaskModelType::from_str("grok_image").unwrap(), TaskModelType::GrokImage);
assert_eq!(TaskModelType::from_str("recraft_3").unwrap(), TaskModelType::Recraft3);
@@ -284,7 +300,7 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = TaskModelType::all_variants();
assert_eq!(variants.len(), 23);
assert_eq!(variants.len(), 25);
// Image models
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Dev));
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Schnell));
@@ -294,6 +310,8 @@ mod tests {
assert_eq!(variants.pop_first(), Some(TaskModelType::FluxPro11Ultra));
assert_eq!(variants.pop_first(), Some(TaskModelType::FluxProKontextMax));
assert_eq!(variants.pop_first(), Some(TaskModelType::Gemini25Flash));
assert_eq!(variants.pop_first(), Some(TaskModelType::NanoBanana));
assert_eq!(variants.pop_first(), Some(TaskModelType::NanoBananaPro));
assert_eq!(variants.pop_first(), Some(TaskModelType::GptImage1));
assert_eq!(variants.pop_first(), Some(TaskModelType::GrokImage));
assert_eq!(variants.pop_first(), Some(TaskModelType::Recraft3));
@@ -1,6 +1,6 @@
import { useState, useRef, useEffect, useCallback } from "react";
import Konva from "konva"; // just for types
import { EnqueueImageInpaint } from "@storyteller/tauri-api";
import { EnqueueEditImage, EnqueueImageInpaint } from "@storyteller/tauri-api";
import { ContextMenuContainer } from "../PageDraw/components/ui/ContextMenu";
import { useCopyPasteHotkeys } from "../PageDraw/hooks/useCopyPasteHotkeys";
import { useDeleteHotkeys } from "../PageDraw/hooks/useDeleteHotkeys";
@@ -285,15 +285,32 @@ const PageEdit = () => {
`inpaint-${Date.now()}-${Math.random().toString(36).slice(2)}`;
try {
const result = await EnqueueImageInpaint({
model: selectedImageModel,
image_media_token: editedImageToken,
mask_image_raw_bytes: arrayBuffer,
prompt: prompt,
image_count: generationCount,
frontend_caller: "image_editor",
frontend_subscriber_id: subscriberId,
});
let result;
if (selectedImageModel?.editingIsInpainting) {
result = await EnqueueImageInpaint({
model: selectedImageModel,
image_media_token: editedImageToken,
mask_image_raw_bytes: arrayBuffer,
prompt: prompt,
image_count: generationCount,
frontend_caller: "image_editor",
frontend_subscriber_id: subscriberId,
});
} else {
result = await EnqueueEditImage({
model: selectedImageModel,
image_media_tokens: [editedImageToken],
disable_system_prompt: true, // No meaning yet
prompt: prompt,
image_count: generationCount,
frontend_caller: "image_editor",
frontend_subscriber_id: subscriberId,
//scene_image_media_token,
//aspect_ratio: aspectRatio,
});
}
if (result.status === "success") {
setPendingGenerations((prev) => [
...prev,
@@ -310,7 +327,8 @@ const PageEdit = () => {
[generationCount, selectedImageModel, store.baseImageInfo?.mediaToken],
);
const isNanoBananaModel = selectedImageModel?.id === "gemini_25_flash";
const isNanoBananaModel = selectedImageModel?.isNanoBananaModel();
const supportsMaskedInpainting =
selectedImageModel?.usesInpaintingMask ?? false;
@@ -28,8 +28,8 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { PromptsApi } from "@storyteller/api";
import {
EnqueueContextualEditImage,
EnqueueContextualEditImageSize,
EnqueueEditImage,
EnqueueEditImageSize,
} from "@storyteller/tauri-api";
import { usePrompt2DStore } from "./promptStore";
import { gtagEvent } from "@storyteller/google-analytics";
@@ -235,7 +235,7 @@ export const PromptBox2D = ({
const aspectRatio = getCurrentAspectRatio();
const generateResponse = await EnqueueContextualEditImage({
const generateResponse = await EnqueueEditImage({
model: selectedImageModel,
scene_image_media_token: snapshotMediaToken.data!,
image_media_tokens: referenceImages.map((image) => image.mediaToken),
@@ -354,16 +354,16 @@ export const PromptBox2D = ({
return iconElement.props.icon;
};
const getCurrentAspectRatio = (): EnqueueContextualEditImageSize => {
const getCurrentAspectRatio = (): EnqueueEditImageSize => {
const selected = aspectRatioList.find((item) => item.selected);
switch (selected?.label) {
case "3:2":
return EnqueueContextualEditImageSize.Wide;
return EnqueueEditImageSize.Wide;
case "2:3":
return EnqueueContextualEditImageSize.Tall;
return EnqueueEditImageSize.Tall;
case "1:1":
default:
return EnqueueContextualEditImageSize.Square;
return EnqueueEditImageSize.Square;
}
};
@@ -38,8 +38,8 @@ import { Modal } from "@storyteller/ui-modal";
import { Signal } from "@preact/signals-react";
import {
CommandSuccessStatus,
EnqueueContextualEditImage,
EnqueueContextualEditImageSize,
EnqueueEditImage,
EnqueueEditImageSize,
} from "@storyteller/tauri-api";
import { usePrompt3DStore } from "./promptStore";
import { gtagEvent } from "@storyteller/google-analytics";
@@ -336,7 +336,7 @@ export const PromptBox3D = ({
const aspectRatio = getCurrentAspectRatio();
const generateResponse = await EnqueueContextualEditImage({
const generateResponse = await EnqueueEditImage({
model: selectedImageModel,
scene_image_media_token: snapshotResult.data!,
image_media_tokens: referenceImages.map((image) => image.mediaToken),
@@ -400,16 +400,16 @@ export const PromptBox3D = ({
}
};
const getCurrentAspectRatio = (): EnqueueContextualEditImageSize => {
const getCurrentAspectRatio = (): EnqueueEditImageSize => {
switch (cameraAspectRatio.value) {
case CameraAspectRatio.HORIZONTAL_3_2:
return EnqueueContextualEditImageSize.Wide;
return EnqueueEditImageSize.Wide;
case CameraAspectRatio.VERTICAL_2_3:
case CameraAspectRatio.VERTICAL_9_16:
return EnqueueContextualEditImageSize.Tall;
return EnqueueEditImageSize.Tall;
case CameraAspectRatio.SQUARE_1_1:
default:
return EnqueueContextualEditImageSize.Square;
return EnqueueEditImageSize.Square;
}
};
@@ -21,6 +21,11 @@ export class ImageModel extends Model {
// For inpainting models, does it require sending a mask?
readonly usesInpaintingMask: boolean;
// For editing models, is "editing" == "inpainting"?
// Examples of "true" are "flux dev juggernaut"
// Examples of "false" are "nano_banana_pro", which is just "editing".
readonly editingIsInpainting: boolean;
// Whether this model supports additional image prompts (reference images)
readonly canUseImagePrompt: boolean;
@@ -44,6 +49,7 @@ export class ImageModel extends Model {
defaultGenerationCount: number;
canEditImages?: boolean;
usesInpaintingMask?: boolean;
editingIsInpainting?: boolean;
canUseImagePrompt?: boolean;
maxImagePromptCount?: number;
canTextToImage?: boolean;
@@ -66,8 +72,24 @@ export class ImageModel extends Model {
this.defaultGenerationCount = args.defaultGenerationCount;
this.canEditImages = args.canEditImages ?? false;
this.usesInpaintingMask = args.usesInpaintingMask ?? false;
this.editingIsInpainting = args.editingIsInpainting ?? false;
this.canUseImagePrompt = args.canUseImagePrompt ?? false;
this.maxImagePromptCount = Math.max(0, args.maxImagePromptCount ?? 1);
this.canTextToImage = args.canTextToImage === false ? false : true; // Default to true !
}
// If the model is a "Nano Banana"-type model, we may want to enable certain features.
// For example, in the editor, we may want to use the marker.
// TODO: Rather than "isNanoBananaModel()", we should have: "enableEditorMarker" as it's
// more semantic.
isNanoBananaModel(): boolean {
switch(this.id) {
case "gemini_25_flash":
case "nano_banana":
case "nano_banana_pro":
return true;
default:
return false;
}
}
}
@@ -134,6 +134,21 @@ const ALL_MODELS: ModelConfig[] = [
},
tags: [ModelTag.InstructiveEdit, ModelTag.NonMaskedInpainting],
}),
cfg({
id: "nano_banana_pro",
category: "image",
info: {
name: "Nano Banana Pro",
tauri_id: "nano_banana_pro",
creator: mc.Google,
},
description: "Extremely powerful",
badges: [{ label: "35 sec." }],
capabilities: {
maxGenerationCount: 4,
},
tags: [ModelTag.InstructiveEdit, ModelTag.NonMaskedInpainting],
}),
cfg({
id: "flux_pro_inpaint",
category: "image",
@@ -2,6 +2,8 @@ import { ImageModel } from "../classes/ImageModel.js";
import { ModelCreator } from "../classes/metadata/ModelCreator.js";
import { ModelTag } from "../classes/metadata/ModelTag.js";
// TODO: Some of the model configs, such as generation counts, are authoritatively controlled in `legacy/Models.ts`
export const IMAGE_MODELS: ImageModel[] = [
new ImageModel({
id: "grok_image",
@@ -111,6 +113,24 @@ export const IMAGE_MODELS: ImageModel[] = [
tags: [ModelTag.InstructiveEdit],
progressBarTime: 25000,
}),
new ImageModel({
id: "nano_banana_pro",
tauriId: "nano_banana_pro",
fullName: "Nano Banana Pro",
category: "image",
creator: ModelCreator.Google,
selectorName: "Nano Banana Pro",
selectorDescription: "Extremely powerful",
selectorBadges: ["30 sec."],
maxGenerationCount: 4,
defaultGenerationCount: 1,
canEditImages: true,
canUseImagePrompt: true,
maxImagePromptCount: 4,
canTextToImage: true,
tags: [ModelTag.InstructiveEdit],
progressBarTime: 25000,
}),
new ImageModel({
id: "gpt_image_1",
tauriId: "gpt_image_1",
@@ -120,7 +140,7 @@ export const IMAGE_MODELS: ImageModel[] = [
selectorName: "GPT Image 1 (GPT-4o)",
selectorDescription: "Slow, but super smart",
selectorBadges: ["60 sec."],
maxGenerationCount: 1,
maxGenerationCount: 4,
defaultGenerationCount: 1,
tags: [ModelTag.InstructiveEdit],
canEditImages: true,
@@ -150,13 +170,14 @@ export const IMAGE_MODELS: ImageModel[] = [
fullName: "Flux Pro Inpaint",
category: "image",
creator: ModelCreator.BlackForestLabs,
selectorName: "Flux Pro",
selectorName: "Flux Pro (Inpainting)",
selectorDescription: "Fast inpainting",
selectorBadges: ["30 sec."],
maxGenerationCount: 1, // NB: Fal only allows one image for some reason!
defaultGenerationCount: 1, // NB: Fal only allows one image for some reason!
canEditImages: true,
usesInpaintingMask: true,
editingIsInpainting: true,
canTextToImage: false,
progressBarTime: 30000,
}),
@@ -173,6 +194,7 @@ export const IMAGE_MODELS: ImageModel[] = [
defaultGenerationCount: 4,
canEditImages: true,
usesInpaintingMask: true,
editingIsInpainting: true,
canTextToImage: false,
progressBarTime: 10000,
}),
+1 -1
View File
@@ -2,7 +2,7 @@ export * from "./lib/artcraft/ArtcraftGetCredits.js";
export * from "./lib/artcraft/ArtcraftGetSubscription.js";
export * from "./lib/artcraft/media_files/MediaFileDelete.js";
export * from "./lib/common/CommandStatus.js";
export * from "./lib/enqueue/EnqueueContextualEditImage.js";
export * from "./lib/enqueue/EnqueueEditImage.js";
export * from "./lib/enqueue/EnqueueImageBgRemovalCommand.js";
export * from "./lib/enqueue/EnqueueImageInpaint.js";
export * from "./lib/enqueue/EnqueueImageTo3dObject.js";
@@ -2,10 +2,10 @@ import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
import { ImageModel } from "@storyteller/model-list";
export interface EnqueueContextualEditImageRequest {
export interface EnqueueEditImageRequest {
/// The model to use.
model?: ImageModel | EnqueueContextualEditImageModel;
model?: ImageModel | EnqueueEditImageModel;
/// If set, this is the first image in the contextual image set.
/// This gets submitted along with `image_media_tokens` and will
@@ -26,26 +26,37 @@ export interface EnqueueContextualEditImageRequest {
/// Number of images to generate.
image_count?: number;
aspect_ratio?: EnqueueContextualEditImageSize;
aspect_ratio?: EnqueueEditImageSize;
image_quality?: EnqueueContextualEditImageQuality;
image_quality?: EnqueueEditImageQuality;
/// Image resolution. Support: Nano Banana Pro
image_resolution?: EnqueueEditImageResolution;
// TODO: Actual enum.
frontend_caller?: string;
// Optional frontend state to return later.
frontend_subscriber_id?: string;
}
// Shape of request sent to Tauri
// (We do some transformations from the public-facing request object.)
interface RawEnqueueContextualEditImageRequest {
model?: EnqueueContextualEditImageModel | string;
interface RawEnqueueEditImageRequest {
model?: EnqueueEditImageModel | string;
scene_image_media_token?: string;
image_media_tokens?: string[];
prompt?: string;
disable_system_prompt?: boolean;
image_count?: number;
aspect_ratio?: EnqueueContextualEditImageSize;
image_quality?: EnqueueContextualEditImageQuality;
aspect_ratio?: EnqueueEditImageSize;
image_quality?: EnqueueEditImageQuality;
frontend_caller?: string;
frontend_subscriber_id?: string;
}
export enum EnqueueContextualEditImageErrorType {
export enum EnqueueEditImageErrorType {
ModelNotSpecified = "model_not_specified",
NoProviderAvailable = "no_provider_available",
BadRequest = "bad_request",
@@ -56,40 +67,46 @@ export enum EnqueueContextualEditImageErrorType {
SoraIsHavingProblems = "sora_is_having_problems",
}
export enum EnqueueContextualEditImageModel {
export enum EnqueueEditImageModel {
GptImage1 = "gpt_image_1",
FluxProKontextMax = "flux_pro_kontext_max",
}
export enum EnqueueContextualEditImageSize {
export enum EnqueueEditImageSize {
Auto = "auto",
Square = "square",
Wide = "wide",
Tall = "tall",
}
export enum EnqueueContextualEditImageQuality {
export enum EnqueueEditImageQuality {
Auto = "auto",
High = "high",
Medium = "medium",
Low = "low",
}
export interface EnqueueContextualEditImageError extends CommandResult {
error_type: EnqueueContextualEditImageErrorType;
export enum EnqueueEditImageResolution {
OneK = "one_k",
TwoK = "two_k",
FourK = "four_k",
}
export interface EnqueueEditImageError extends CommandResult {
error_type: EnqueueEditImageErrorType;
error_message?: string;
}
export interface EnqueueContextualEditImagePayload {
export interface EnqueueEditImagePayload {
}
export interface EnqueueContextualEditImageSuccess extends CommandResult {
payload: EnqueueContextualEditImagePayload;
export interface EnqueueEditImageSuccess extends CommandResult {
payload: EnqueueEditImagePayload;
}
export type EnqueueContextualEditImageResult = EnqueueContextualEditImageSuccess | EnqueueContextualEditImageError;
export type EnqueueEditImageResult = EnqueueEditImageSuccess | EnqueueEditImageError;
export const EnqueueContextualEditImage = async (request: EnqueueContextualEditImageRequest) : Promise<EnqueueContextualEditImageResult> => {
export const EnqueueEditImage = async (request: EnqueueEditImageRequest) : Promise<EnqueueEditImageResult> => {
let modelName = undefined;
if (!!request.model) {
@@ -105,7 +122,7 @@ export const EnqueueContextualEditImage = async (request: EnqueueContextualEditI
throw new Error("No model specified in request: " + JSON.stringify(request));
}
let mutableRequest : RawEnqueueContextualEditImageRequest = {
let mutableRequest : RawEnqueueEditImageRequest = {
model: modelName,
prompt: request.prompt,
};
@@ -134,9 +151,17 @@ export const EnqueueContextualEditImage = async (request: EnqueueContextualEditI
mutableRequest.disable_system_prompt = request.disable_system_prompt;
}
const result = await invoke("enqueue_contextual_edit_image_command", {
if (!!request.frontend_caller) {
mutableRequest.frontend_caller = request.frontend_caller;
}
if (!!request.frontend_subscriber_id) {
mutableRequest.frontend_subscriber_id = request.frontend_subscriber_id;
}
const result = await invoke("enqueue_edit_image_command", {
request: mutableRequest,
});
return (result as EnqueueContextualEditImageResult);
return (result as EnqueueEditImageResult);
}
+5
View File
@@ -45,6 +45,11 @@ sqlx_offline() {
#sqlx_migrate
sqlx_offline
# Issue with Linux performance
# https://github.com/tauri-apps/tauri/issues/13498
export WEBKIT_DISABLE_DMABUF_RENDERER=1
export WEBKIT_DISABLE_COMPOSITING_MODE=1
echo "Build and run..."
RUSTFLAGS="-Awarnings" cargo tauri dev \
--no-dev-server \