NBP via artcraft_router in Tauri; image cost estimates in tauri (#1075)

This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-03-02 05:13:23 -05:00
1 parent bc91785628
commit aab8cbdfe1
16 files changed
+387 -298

No files matched your search

@@ -0,0 +1,20 @@
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::cost_estimate::estimate_image_cost::{
EstimateImageCostRequest, EstimateImageCostResponse, ESTIMATE_IMAGE_COST_PATH,
};
pub async fn estimate_image_cost(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: EstimateImageCostRequest,
) -> Result<EstimateImageCostResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
ESTIMATE_IMAGE_COST_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1 @@
pub mod estimate_image_cost;
@@ -1 +1,2 @@
pub mod image;
pub mod video;
@@ -19,7 +19,16 @@ pub async fn basic_json_post_request<Req: Serialize, Res: DeserializeOwned>(
let url = get_route(api_host, route_path);
info!("Requesting {:?}", &url);
// NB: We record lightweight analytics telemetry to understand which areas
// of the tool are useful and which models and services are important.
let debug_log = url.ends_with("/v1/analytics/active_user_v2");
// Suppress "spammy" logs at the debug level.
if debug_log {
debug!("Requesting {:?}", &url);
} else {
info!("Requesting {:?}", &url);
}
let client = Client::builder()
.gzip(true)
@@ -0,0 +1,42 @@
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::ResponseOrError;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use artcraft_api_defs::generate::cost_estimate::estimate_image_cost::{
EstimateImageCostError, EstimateImageCostErrorType, EstimateImageCostRequest,
EstimateImageCostResponse,
};
use log::info;
use artcraft_client::endpoints::generate::cost_estimate::image::estimate_image_cost::estimate_image_cost;
use tauri::State;
impl SerializeMarker for EstimateImageCostResponse {}
#[tauri::command]
pub async fn estimate_image_cost_command(
request: EstimateImageCostRequest,
app_env_configs: State<'_, AppEnvConfigs>,
) -> ResponseOrError<EstimateImageCostResponse, EstimateImageCostError> {
info!("estimate_image_cost_command called");
let result = estimate_image_cost(
&app_env_configs.storyteller_host,
None, // Credentials are not required for this endpoint.
request,
)
.await;
match result {
Ok(response) => Ok(response.into()),
Err(err) => Err(CommandErrorResponseWrapper {
status: CommandErrorStatus::BadRequest,
error_message: None,
error_type: None,
error_details: Some(EstimateImageCostError {
success: false,
error_type: EstimateImageCostErrorType::InvalidInput,
error_message: err.to_string(),
}),
}),
}
}
@@ -1 +1,2 @@
pub mod estimate_image_cost_command;
pub mod estimate_video_cost_command;
@@ -1,148 +0,0 @@
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_nano_banana_pro::aspect_ratio_to_artcraft_nano_banana_pro;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::ImageEditModel::NanoBanana;
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::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, 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 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::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 artcraft_client::endpoints::generate::image::edit::flux_pro_kontext_max_edit_image::flux_pro_kontext_max_edit_image;
use artcraft_client::endpoints::generate::image::edit::gemini_25_flash_edit_image::gemini_25_flash_edit_image;
use artcraft_client::endpoints::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
use artcraft_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(super) const MAX_IMAGES: usize = 10;
pub async fn handle_artcraft_nano_banana_pro_edit(
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 = get_aspect_ratio(request);
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,
})
}
fn get_aspect_ratio(request: &EnqueueEditImageCommand) -> Option<NanoBananaProMultiFunctionImageGenAspectRatio> {
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
// Handle modern aspect ratio
let aspect = aspect_ratio_to_artcraft_nano_banana_pro(common_aspect_ratio);
return Some(aspect);
}
if let Some(aspect_ratio) = request.aspect_ratio {
// Handle deprecated aspect ratio
return match aspect_ratio {
EditImageSize::Auto => Some(NanoBananaProMultiFunctionImageGenAspectRatio::Auto),
EditImageSize::Square => Some(NanoBananaProMultiFunctionImageGenAspectRatio::OneByOne),
EditImageSize::Wide => Some(NanoBananaProMultiFunctionImageGenAspectRatio::SixteenByNine),
EditImageSize::Tall => Some(NanoBananaProMultiFunctionImageGenAspectRatio::NineBySixteen),
}
}
None
}
@@ -3,14 +3,16 @@ use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_flux_k
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_gpt_image_1_edit::handle_artcraft_gpt_image_1_edit;
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_gpt_image_1p5_edit::handle_artcraft_gpt_image_1p5_edit;
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_nano_banana_edit::handle_artcraft_nano_banana_edit;
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_nano_banana_pro_edit::handle_artcraft_nano_banana_pro_edit;
use crate::core::commands::enqueue::image_edit::artcraft::handle_image_edit_artcraft_via_router::handle_image_edit_artcraft_via_router;
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_seedream_4_edit::handle_artcraft_seedream_4_edit;
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_seedream_4p5_edit::handle_artcraft_seedream_4p5_edit;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EnqueueEditImageCommand, ImageEditModel};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_router::api::common_image_model::CommonImageModel;
use tauri::AppHandle;
pub async fn handle_image_edit_artcraft(
@@ -27,7 +29,7 @@ pub async fn handle_image_edit_artcraft(
ImageEditModel::Gemini25Flash | ImageEditModel::NanoBanana => {
handle_artcraft_nano_banana_edit( request, app, app_data_root, app_env_configs, storyteller_creds_manager).await
},
ImageEditModel::NanoBananaPro => handle_artcraft_nano_banana_pro_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await,
ImageEditModel::NanoBananaPro => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBananaPro, GenerationModel::NanoBananaPro).await,
ImageEditModel::GptImage1 => handle_artcraft_gpt_image_1_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await,
ImageEditModel::GptImage1p5 => handle_artcraft_gpt_image_1p5_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await,
ImageEditModel::Seedream4 => handle_artcraft_seedream_4_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await,
@@ -0,0 +1,153 @@
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio as DesktopCommonAspectRatio;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{
EditImageResolution, EditImageSize, EnqueueEditImageCommand,
};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterCommonAspectRatio;
use artcraft_router::api::common_image_model::CommonImageModel;
use artcraft_router::api::common_resolution::CommonResolution as RouterCommonResolution;
use artcraft_router::api::image_list_ref::ImageListRef;
use artcraft_router::api::provider::Provider;
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
use artcraft_router::client::router_client::RouterClient;
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use log::{error, info};
pub(super) const MAX_EDIT_IMAGES: usize = 10;
pub(super) async fn handle_image_edit_artcraft_via_router(
request: &EnqueueEditImageCommand,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
model: CommonImageModel,
generation_model: GenerationModel,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials)),
};
let client = RouterClient::Artcraft(RouterArtcraftClient::new(
app_env_configs.storyteller_host.clone(),
creds,
));
let mut media_tokens = Vec::with_capacity(MAX_EDIT_IMAGES);
if let Some(scene_token) = request.scene_image_media_token.clone() {
media_tokens.push(scene_token);
}
if let Some(tokens) = request.image_media_tokens.as_ref() {
media_tokens.extend_from_slice(tokens);
}
if media_tokens.len() > MAX_EDIT_IMAGES {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages {
min: 1,
max: MAX_EDIT_IMAGES as u32,
provided: media_tokens.len() as u32,
}));
}
let image_inputs = if media_tokens.is_empty() {
None
} else {
Some(ImageListRef::MediaFileTokens(&media_tokens))
};
let aspect_ratio = get_aspect_ratio_edit(request);
let resolution = get_resolution_edit(request);
let router_request = GenerateImageRequest {
model,
provider: Provider::Artcraft,
prompt: Some(request.prompt.as_str()),
image_inputs,
resolution,
aspect_ratio,
image_batch_count: request.image_count.map(|n| n as u16),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
idempotency_token: None,
};
let plan = router_request.build()?;
info!("Image Generation Plan: {:?}", plan);
let response = match plan.generate_image(&client).await {
Ok(resp) => {
info!("Successfully enqueued.");
resp
}
Err(err) => {
error!("Failed to enqueue: {:?}", err);
return Err(GenerateError::from(err));
}
};
let job_id = response
.get_artcraft_payload()
.map(|p| p.inference_job_token.to_string())
.ok_or(GenerateError::ResponseHadNoJobTokens)?;
info!("Job token: {}", job_id);
Ok(TaskEnqueueSuccess {
task_type: TaskType::ImageGeneration,
model: Some(generation_model),
provider: GenerationProvider::Artcraft,
provider_job_id: Some(job_id),
})
}
fn get_aspect_ratio_edit(request: &EnqueueEditImageCommand) -> Option<RouterCommonAspectRatio> {
if let Some(ar) = request.common_aspect_ratio {
return Some(convert_desktop_aspect_ratio(ar));
}
if let Some(ar) = request.aspect_ratio {
return Some(match ar {
EditImageSize::Auto => RouterCommonAspectRatio::Auto,
EditImageSize::Square => RouterCommonAspectRatio::Square,
EditImageSize::Wide => RouterCommonAspectRatio::Wide,
EditImageSize::Tall => RouterCommonAspectRatio::Tall,
});
}
None
}
fn get_resolution_edit(request: &EnqueueEditImageCommand) -> Option<RouterCommonResolution> {
request.image_resolution.map(|res| match res {
EditImageResolution::OneK => RouterCommonResolution::OneK,
EditImageResolution::TwoK => RouterCommonResolution::TwoK,
EditImageResolution::FourK => RouterCommonResolution::FourK,
})
}
fn convert_desktop_aspect_ratio(ar: DesktopCommonAspectRatio) -> RouterCommonAspectRatio {
match ar {
DesktopCommonAspectRatio::Auto => RouterCommonAspectRatio::Auto,
DesktopCommonAspectRatio::Square => RouterCommonAspectRatio::Square,
DesktopCommonAspectRatio::WideThreeByTwo => RouterCommonAspectRatio::WideThreeByTwo,
DesktopCommonAspectRatio::WideFourByThree => RouterCommonAspectRatio::WideFourByThree,
DesktopCommonAspectRatio::WideFiveByFour => RouterCommonAspectRatio::WideFiveByFour,
DesktopCommonAspectRatio::WideSixteenByNine => RouterCommonAspectRatio::WideSixteenByNine,
DesktopCommonAspectRatio::WideTwentyOneByNine => RouterCommonAspectRatio::WideTwentyOneByNine,
DesktopCommonAspectRatio::TallTwoByThree => RouterCommonAspectRatio::TallTwoByThree,
DesktopCommonAspectRatio::TallThreeByFour => RouterCommonAspectRatio::TallThreeByFour,
DesktopCommonAspectRatio::TallFourByFive => RouterCommonAspectRatio::TallFourByFive,
DesktopCommonAspectRatio::TallNineBySixteen => RouterCommonAspectRatio::TallNineBySixteen,
DesktopCommonAspectRatio::TallNineByTwentyOne => RouterCommonAspectRatio::TallNineByTwentyOne,
DesktopCommonAspectRatio::Wide => RouterCommonAspectRatio::Wide,
DesktopCommonAspectRatio::Tall => RouterCommonAspectRatio::Tall,
DesktopCommonAspectRatio::Auto2k => RouterCommonAspectRatio::Auto2k,
DesktopCommonAspectRatio::Auto4k => RouterCommonAspectRatio::Auto4k,
DesktopCommonAspectRatio::SquareHd => RouterCommonAspectRatio::SquareHd,
}
}
@@ -1,8 +1,8 @@
mod handle_image_edit_artcraft_via_router;
mod handle_artcraft_flux_kontext_edit;
mod handle_artcraft_gpt_image_1_edit;
mod handle_artcraft_gpt_image_1p5_edit;
mod handle_artcraft_nano_banana_edit;
mod handle_artcraft_nano_banana_pro_edit;
mod handle_artcraft_seedream_4_edit;
mod handle_artcraft_seedream_4p5_edit;
@@ -1,141 +0,0 @@
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio;
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_nano_banana_pro::aspect_ratio_to_artcraft_nano_banana_pro;
use crate::core::api_adapters::resolution::common_resolution::CommonResolution;
use crate::core::api_adapters::resolution::convert::resolution_to_artcraft_nano_banana_pro::resolution_to_artcraft_nano_banana_pro;
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::multi_function::nano_banana_pro_multi_function_image_gen::{NanoBananaProMultiFunctionImageGenAspectRatio, NanoBananaProMultiFunctionImageGenImageResolution, NanoBananaProMultiFunctionImageGenNumImages, NanoBananaProMultiFunctionImageGenRequest};
use artcraft_api_defs::generate::image::text::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 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 artcraft_client::endpoints::generate::image::edit::gemini_25_flash_edit_image::gemini_25_flash_edit_image;
use artcraft_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_artcraft_nano_banana_pro_text_to_image(
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 = get_aspect_ratio(request);
let resolution = get_resolution(request);
let request = NanoBananaProMultiFunctionImageGenRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
image_media_tokens: request.image_media_tokens.clone(),
num_images,
resolution,
aspect_ratio,
};
info!("Calling Artcraft with request {:?}", request);
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,
})
}
fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<NanoBananaProMultiFunctionImageGenAspectRatio> {
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
// Handle modern aspect ratio
let aspect = aspect_ratio_to_artcraft_nano_banana_pro(common_aspect_ratio);
return Some(aspect);
}
if let Some(aspect_ratio) = request.aspect_ratio {
// Handle deprecated aspect ratio
return match aspect_ratio {
TextToImageSize::Auto => Some(NanoBananaProMultiFunctionImageGenAspectRatio::Auto),
TextToImageSize::Square => Some(NanoBananaProMultiFunctionImageGenAspectRatio::OneByOne),
TextToImageSize::Wide => Some(NanoBananaProMultiFunctionImageGenAspectRatio::SixteenByNine),
TextToImageSize::Tall => Some(NanoBananaProMultiFunctionImageGenAspectRatio::NineBySixteen),
}
}
None
}
fn get_resolution(request: &EnqueueTextToImageRequest) -> Option<NanoBananaProMultiFunctionImageGenImageResolution> {
if let Some(common_resolution) = request.common_resolution {
return Some(resolution_to_artcraft_nano_banana_pro(common_resolution));
}
if let Some(resolution) = request.image_resolution {
// Deprecated resolution
return Some(match resolution {
TextToImageResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
TextToImageResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK,
TextToImageResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
})
}
None
}
@@ -6,15 +6,17 @@ use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_flu
use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_flux_pro_1p1_ultra_text_to_image::handle_artcraft_flux_pro_1p1_ultra_text_to_image;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_gpt_image_1_text_to_image::handle_artcraft_gpt_image_1_text_to_image;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_gpt_image_1p5_text_to_image::handle_artcraft_gpt_image_1p5_text_to_image;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_nano_banana_pro_text_to_image::handle_artcraft_nano_banana_pro_text_to_image;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_text_to_image_artcraft_via_router::handle_text_to_image_artcraft_via_router;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_nano_banana_text_to_image::handle_artcraft_nano_banana_text_to_image;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_seedream_4_text_to_image::handle_artcraft_seedream_4_text_to_image;
use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_seedream_4p5_text_to_image::handle_artcraft_seedream_4p5_text_to_image;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageModel};
use crate::core::commands::enqueue::text_to_image::text_to_image_models::text_to_image_model_to_model_type;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_router::api::common_image_model::CommonImageModel;
use enums::common::generation_provider::GenerationProvider;
use tauri::AppHandle;
@@ -46,7 +48,7 @@ pub async fn handle_text_to_image_artcraft(
TextToImageModel::Gemini25Flash | TextToImageModel::NanoBanana => {
handle_artcraft_nano_banana_text_to_image(request, app_env_configs, storyteller_creds_manager).await
},
TextToImageModel::NanoBananaPro => handle_artcraft_nano_banana_pro_text_to_image(request, app_env_configs, storyteller_creds_manager).await,
TextToImageModel::NanoBananaPro => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBananaPro, GenerationModel::NanoBananaPro).await,
TextToImageModel::Seedream4 => handle_artcraft_seedream_4_text_to_image(request, app_env_configs, storyteller_creds_manager).await,
TextToImageModel::Seedream4p5 => handle_artcraft_seedream_4p5_text_to_image(request, app_env_configs, storyteller_creds_manager).await,
}
@@ -0,0 +1,144 @@
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio as CommonAspectRatio2;
use crate::core::api_adapters::resolution::common_resolution::CommonResolution as CommonResolution2;
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
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::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterCommonAspectRatio;
use artcraft_router::api::common_image_model::CommonImageModel;
use artcraft_router::api::common_resolution::CommonResolution as RouterCommonResolution;
use artcraft_router::api::image_list_ref::ImageListRef;
use artcraft_router::api::provider::Provider;
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
use artcraft_router::client::router_client::RouterClient;
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use log::{error, info};
pub(super) async fn handle_text_to_image_artcraft_via_router(
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
model: CommonImageModel,
generation_model: GenerationModel,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials)),
};
let client = RouterClient::Artcraft(RouterArtcraftClient::new(
app_env_configs.storyteller_host.clone(),
creds,
));
let image_inputs = request.image_media_tokens.as_ref().map(ImageListRef::MediaFileTokens);
let aspect_ratio = get_aspect_ratio_t2i(request);
let resolution = get_resolution_t2i(request);
let router_request = GenerateImageRequest {
model,
provider: Provider::Artcraft,
prompt: request.prompt.as_deref(),
image_inputs,
resolution,
aspect_ratio,
image_batch_count: request.number_images.map(|n| n as u16),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
idempotency_token: None,
};
let plan = router_request.build()?;
info!("Image Generation Plan: {:?}", plan);
let response = match plan.generate_image(&client).await {
Ok(resp) => {
info!("Successfully enqueued.");
resp
}
Err(err) => {
error!("Failed to enqueue: {:?}", err);
return Err(GenerateError::from(err));
}
};
let job_id = response
.get_artcraft_payload()
.map(|p| p.inference_job_token.to_string())
.ok_or(GenerateError::ResponseHadNoJobTokens)?;
Ok(TaskEnqueueSuccess {
task_type: TaskType::ImageGeneration,
model: Some(generation_model),
provider: GenerationProvider::Artcraft,
provider_job_id: Some(job_id),
})
}
fn get_aspect_ratio_t2i(request: &EnqueueTextToImageRequest) -> Option<RouterCommonAspectRatio> {
if let Some(ar) = request.common_aspect_ratio {
return Some(convert_desktop_aspect_ratio(ar));
}
if let Some(ar) = request.aspect_ratio {
return Some(match ar {
TextToImageSize::Auto => RouterCommonAspectRatio::Auto,
TextToImageSize::Square => RouterCommonAspectRatio::Square,
TextToImageSize::Wide => RouterCommonAspectRatio::Wide,
TextToImageSize::Tall => RouterCommonAspectRatio::Tall,
});
}
None
}
fn get_resolution_t2i(request: &EnqueueTextToImageRequest) -> Option<RouterCommonResolution> {
if let Some(res) = request.common_resolution {
return Some(convert_desktop_resolution(res));
}
if let Some(res) = request.image_resolution {
return Some(match res {
TextToImageResolution::OneK => RouterCommonResolution::OneK,
TextToImageResolution::TwoK => RouterCommonResolution::TwoK,
TextToImageResolution::FourK => RouterCommonResolution::FourK,
});
}
None
}
fn convert_desktop_aspect_ratio(ar: CommonAspectRatio2) -> RouterCommonAspectRatio {
match ar {
CommonAspectRatio2::Auto => RouterCommonAspectRatio::Auto,
CommonAspectRatio2::Square => RouterCommonAspectRatio::Square,
CommonAspectRatio2::WideThreeByTwo => RouterCommonAspectRatio::WideThreeByTwo,
CommonAspectRatio2::WideFourByThree => RouterCommonAspectRatio::WideFourByThree,
CommonAspectRatio2::WideFiveByFour => RouterCommonAspectRatio::WideFiveByFour,
CommonAspectRatio2::WideSixteenByNine => RouterCommonAspectRatio::WideSixteenByNine,
CommonAspectRatio2::WideTwentyOneByNine => RouterCommonAspectRatio::WideTwentyOneByNine,
CommonAspectRatio2::TallTwoByThree => RouterCommonAspectRatio::TallTwoByThree,
CommonAspectRatio2::TallThreeByFour => RouterCommonAspectRatio::TallThreeByFour,
CommonAspectRatio2::TallFourByFive => RouterCommonAspectRatio::TallFourByFive,
CommonAspectRatio2::TallNineBySixteen => RouterCommonAspectRatio::TallNineBySixteen,
CommonAspectRatio2::TallNineByTwentyOne => RouterCommonAspectRatio::TallNineByTwentyOne,
CommonAspectRatio2::Wide => RouterCommonAspectRatio::Wide,
CommonAspectRatio2::Tall => RouterCommonAspectRatio::Tall,
CommonAspectRatio2::Auto2k => RouterCommonAspectRatio::Auto2k,
CommonAspectRatio2::Auto4k => RouterCommonAspectRatio::Auto4k,
CommonAspectRatio2::SquareHd => RouterCommonAspectRatio::SquareHd,
}
}
fn convert_desktop_resolution(res: CommonResolution2) -> RouterCommonResolution {
match res {
CommonResolution2::OneK => RouterCommonResolution::OneK,
CommonResolution2::TwoK => RouterCommonResolution::TwoK,
CommonResolution2::ThreeK => RouterCommonResolution::ThreeK,
CommonResolution2::FourK => RouterCommonResolution::FourK,
}
}
@@ -1,8 +1,8 @@
pub (super) mod handle_text_to_image_artcraft;
mod handle_text_to_image_artcraft_via_router;
mod handle_artcraft_gpt_image_1_text_to_image;
mod handle_artcraft_gpt_image_1p5_text_to_image;
mod handle_artcraft_nano_banana_pro_text_to_image;
mod handle_artcraft_nano_banana_text_to_image;
mod handle_artcraft_seedream_4_text_to_image;
mod handle_artcraft_seedream_4p5_text_to_image;
@@ -13,7 +13,7 @@ use enums::tauri::tasks::task_model_type::TaskModelType;
use enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
use errors::AnyhowResult;
use log::{error, info, warn};
use log::{debug, error, info, warn};
use serde_derive::{Deserialize, Serialize};
use sqlite_tasks::queries::list_tasks_for_frontend::list_tasks_for_frontend;
use artcraft_client::endpoints::media_files::delete_media_file::delete_media_file;
@@ -72,7 +72,8 @@ pub async fn get_task_queue_command(
task_database: State<'_, TaskDatabase>,
) -> ResponseOrErrorMessage<GetTaskQueueCommandResponse> {
info!("get_task_queue_command called");
// NB: This is debug because it spams the logs.
debug!("get_task_queue_command called");
let result = handle_request(
&task_database,
+2
View File
@@ -6,6 +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::cost_estimate::estimate_image_cost_command::estimate_image_cost_command;
use crate::core::commands::cost_estimate::estimate_video_cost_command::estimate_video_cost_command;
use crate::core::commands::download::download_directory_reveal_command::download_directory_reveal_command;
use crate::core::commands::download::download_media_file_command::download_media_file_command;
@@ -212,6 +213,7 @@ pub fn run() {
enqueue_image_to_gaussian_command,
enqueue_image_to_video_command,
enqueue_text_to_image_command,
estimate_image_cost_command,
estimate_video_cost_command,
flip_image,
get_app_info_command,