cleanup / remove some old code that isn't used

This commit is contained in:
Hanashi 2026-08-03 23:44:15 -04:00
parent b82fbe0147
commit d77d243dcd
115 changed files with 122 additions and 6493 deletions

View File

@ -1,4 +1,3 @@
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::EnqueueEditImageSuccessResponse;
use crate::commands::response::shorthand::ResponseOrErrorMessage;
use crate::commands::response::success_response_wrapper::SerializeMarker;
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;

View File

@ -1,7 +1,6 @@
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::commands::background_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;

View File

@ -1,7 +1,7 @@
use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::enqueue::image_bg_removal::artcraft::handle_generic_bg_removal_artcraft::handle_generic_bg_removal_artcraft;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::generate::generate_error::{BadInputReason, GenerateError};
use crate::commands::background_removal::artcraft::handle_generic_bg_removal_artcraft::handle_generic_bg_removal_artcraft;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::commands::response::shorthand::{Response, ResponseOrErrorType};
use crate::commands::response::success_response_wrapper::SerializeMarker;

View File

@ -1,131 +0,0 @@
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::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_client::api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{FluxProKontextMaxEditImageNumImages, FluxProKontextMaxEditImageRequest};
use artcraft_client::api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use uuid_utils::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::gpt_image_1_edit_image::gpt_image_1_edit_image;
use tauri::AppHandle;
pub async fn handle_artcraft_flux_kontext_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 flux kontext max edit (edit) ...");
let uuid_idempotency_token = generate_random_uuid();
let num_images = match request.image_count {
None => None,
Some(1) => Some(FluxProKontextMaxEditImageNumImages::One),
Some(2) => Some(FluxProKontextMaxEditImageNumImages::Two),
Some(3) => Some(FluxProKontextMaxEditImageNumImages::Three),
Some(4) => Some(FluxProKontextMaxEditImageNumImages::Four),
Some(other) => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
}));
},
};
let mut maybe_media_token = None;
let list_count = request.image_media_tokens
.as_ref()
.map(|tokens| tokens.len())
.unwrap_or(0);
if request.scene_image_media_token.is_some() && list_count > 0 {
return Err(GenerateError::BadInput(BadInputReason::WrongImageArguments(
"Cannot specify both scene_image_media_token and image_media_tokens for Flux Kontext Max".to_string())));
}
if list_count > 1 {
return Err(GenerateError::BadInput(BadInputReason::WrongImageArguments(
"Cannot specify more than one image in image_media_tokens for Flux Kontext Max".to_string())));
}
if let Some(media_token) = &request.scene_image_media_token {
maybe_media_token = Some(media_token.clone());
}
let maybe_list_media_token = request.image_media_tokens
.as_ref()
.map(|tokens| tokens.first())
.flatten();
if let Some(media_token) = maybe_list_media_token {
maybe_media_token = Some(media_token.clone());
}
let media_token = match maybe_media_token {
Some(token) => token,
None => {
return Err(GenerateError::BadInput(BadInputReason::WrongImageArguments(
"No input image specified for Flux Kontext Max edit".to_string())));
},
};
let request = FluxProKontextMaxEditImageRequest {
uuid_idempotency_token,
prompt: Some(request.prompt.clone()),
image_media_token: media_token,
num_images,
};
let result = flux_pro_kontext_max_edit_image(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft gpt-image-1. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::FluxProKontextMax),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}

View File

@ -1,129 +0,0 @@
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::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_client::api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use uuid_utils::uuid::generate_random_uuid;
use log::{error, info};
use artcraft_client::endpoints::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
use tauri::AppHandle;
const MAX_IMAGES: usize = 4;
pub async fn handle_artcraft_gpt_image_1_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 gpt-image-1 (edit) ...");
let uuid_idempotency_token = generate_random_uuid();
let image_quality = request.image_quality
.map(|quality| match quality {
EditImageQuality::Auto => GptImage1EditImageImageQuality::Auto,
EditImageQuality::High => GptImage1EditImageImageQuality::High,
EditImageQuality::Medium => GptImage1EditImageImageQuality::Medium,
EditImageQuality::Low => GptImage1EditImageImageQuality::Low,
});
let image_size = request.aspect_ratio
.map(|size| match size {
EditImageSize::Auto => GptImage1EditImageImageSize::Square,
EditImageSize::Square => GptImage1EditImageImageSize::Square,
EditImageSize::Tall => GptImage1EditImageImageSize::Vertical,
EditImageSize::Wide => GptImage1EditImageImageSize::Horizontal,
});
let num_images = match request.image_count {
None => None,
Some(1) => Some(GptImage1EditImageNumImages::One),
Some(2) => Some(GptImage1EditImageNumImages::Two),
Some(3) => Some(GptImage1EditImageNumImages::Three),
Some(4) => Some(GptImage1EditImageNumImages::Four),
Some(other) => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
}));
},
};
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 = GptImage1EditImageRequest {
uuid_idempotency_token,
prompt: Some(request.prompt.clone()),
image_media_tokens: Some(media_tokens),
image_size,
num_images,
image_quality,
};
let result = gpt_image_1_edit_image(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft gpt-image-1. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::GptImage1),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}

View File

@ -1,38 +0,0 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::deprecated::image_edit::artcraft::handle_artcraft_flux_kontext_edit::handle_artcraft_flux_kontext_edit;
use crate::commands::deprecated::image_edit::artcraft::handle_artcraft_gpt_image_1_edit::handle_artcraft_gpt_image_1_edit;
use crate::commands::deprecated::image_edit::artcraft::handle_image_edit_artcraft_via_router::handle_image_edit_artcraft_via_router;
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{EnqueueEditImageCommand, ImageEditModel};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::generation_events::common::GenerationModel;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use router::api::router_image_model::RouterImageModel;
use tauri::AppHandle;
pub async fn handle_image_edit_artcraft(
model: ImageEditModel,
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
match model {
ImageEditModel::FluxProKontextMax => handle_artcraft_flux_kontext_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await,
ImageEditModel::Gemini25Flash | ImageEditModel::NanoBanana => {
handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::NanoBanana, GenerationModel::NanoBanana).await
},
ImageEditModel::NanoBanana2 => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::NanoBanana2, GenerationModel::NanoBanana2).await,
ImageEditModel::NanoBananaPro => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::NanoBananaPro, 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_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::GptImage1p5, GenerationModel::GptImage1p5).await,
ImageEditModel::Seedream4 => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::Seedream4, GenerationModel::Seedream4).await,
ImageEditModel::Seedream4p5 => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::Seedream4p5, GenerationModel::Seedream4p5).await,
ImageEditModel::Seedream5Lite => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::Seedream5Lite, GenerationModel::Seedream5Lite).await,
ImageEditModel::QwenEdit2511Angles => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::QwenEdit2511Angles, GenerationModel::QwenEdit2511Angles).await,
ImageEditModel::Flux2LoraAngles => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::Flux2LoraAngles, GenerationModel::Flux2LoraAngles).await,
}
}

View File

@ -1,172 +0,0 @@
use crate::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio as DesktopCommonAspectRatio;
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{
EditImageResolution, EditImageSize, EnqueueEditImageCommand,
};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::generation_events::common::GenerationModel;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use router::api::router_aspect_ratio::RouterAspectRatio;
use router::api::router_image_model::RouterImageModel;
use router::api::router_resolution::RouterResolution;
use router::api::image_list_ref::ImageListRef;
use router::api::router_provider::RouterProvider;
use router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use router::client::router_artcraft_client::RouterArtcraftClient;
use router::client::router_client::RouterClient;
use router::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use router::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
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: RouterImageModel,
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 = GenerateImageRequestBuilder {
model,
provider: RouterProvider::Artcraft,
prompt: Some(request.prompt.clone()),
image_inputs,
resolution,
aspect_ratio,
quality: None, // TODO(bt): Add quality
image_batch_count: request.image_count.map(|n| n as u16),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: request.horizontal_angle,
vertical_angle: request.vertical_angle,
zoom: request.zoom,
};
let dor = router_request.build2()?;
info!("Image Generation Request: {:?}", dor);
let request = match dor {
ImageGenerationDraftOrRequest::Request(req) => req,
// Artcraft never returns a Draft — image-token resolution happens server-side.
// The only draft-producing models today are Kinovi-Midjourney variants,
// which are routed to a separate provider.
ImageGenerationDraftOrRequest::Draft(_) => unreachable!(
"Artcraft router should never produce a draft for image edits"
),
};
let response = match request.send_request(&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),
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}
fn get_aspect_ratio_edit(request: &EnqueueEditImageCommand) -> Option<RouterAspectRatio> {
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 => RouterAspectRatio::Auto,
EditImageSize::Square => RouterAspectRatio::Square,
EditImageSize::Wide => RouterAspectRatio::Wide,
EditImageSize::Tall => RouterAspectRatio::Tall,
});
}
None
}
fn get_resolution_edit(request: &EnqueueEditImageCommand) -> Option<RouterResolution> {
request.image_resolution.map(|res| match res {
EditImageResolution::OneK => RouterResolution::OneK,
EditImageResolution::TwoK => RouterResolution::TwoK,
EditImageResolution::FourK => RouterResolution::FourK,
})
}
fn convert_desktop_aspect_ratio(ar: DesktopCommonAspectRatio) -> RouterAspectRatio {
match ar {
DesktopCommonAspectRatio::Auto => RouterAspectRatio::Auto,
DesktopCommonAspectRatio::Square => RouterAspectRatio::Square,
DesktopCommonAspectRatio::WideThreeByTwo => RouterAspectRatio::WideThreeByTwo,
DesktopCommonAspectRatio::WideFourByThree => RouterAspectRatio::WideFourByThree,
DesktopCommonAspectRatio::WideFiveByFour => RouterAspectRatio::WideFiveByFour,
DesktopCommonAspectRatio::WideSixteenByNine => RouterAspectRatio::WideSixteenByNine,
DesktopCommonAspectRatio::WideTwentyOneByNine => RouterAspectRatio::WideTwentyOneByNine,
DesktopCommonAspectRatio::TallTwoByThree => RouterAspectRatio::TallTwoByThree,
DesktopCommonAspectRatio::TallThreeByFour => RouterAspectRatio::TallThreeByFour,
DesktopCommonAspectRatio::TallFourByFive => RouterAspectRatio::TallFourByFive,
DesktopCommonAspectRatio::TallNineBySixteen => RouterAspectRatio::TallNineBySixteen,
DesktopCommonAspectRatio::TallNineByTwentyOne => RouterAspectRatio::TallNineByTwentyOne,
DesktopCommonAspectRatio::Wide => RouterAspectRatio::Wide,
DesktopCommonAspectRatio::Tall => RouterAspectRatio::Tall,
DesktopCommonAspectRatio::Auto2k => RouterAspectRatio::Auto2k,
DesktopCommonAspectRatio::Auto4k => RouterAspectRatio::Auto4k,
DesktopCommonAspectRatio::SquareHd => RouterAspectRatio::SquareHd,
}
}

View File

@ -1,5 +0,0 @@
mod handle_image_edit_artcraft_via_router;
mod handle_artcraft_flux_kontext_edit;
mod handle_artcraft_gpt_image_1_edit;
pub (super) mod handle_image_edit_artcraft;

View File

@ -1,424 +0,0 @@
use crate::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio;
use crate::api_adapters::quality::common_quality::CommonQuality;
use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::deprecated::image_edit::artcraft::handle_image_edit_artcraft::handle_image_edit_artcraft;
use crate::commands::deprecated::image_edit::image_edit_models::image_edit_model_to_model_type;
use crate::commands::deprecated::image_edit::sora::handle_image_edit_sora::handle_image_edit_sora;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::commands::response::shorthand::{Response, ResponseOrErrorType};
use crate::commands::response::success_response_wrapper::SerializeMarker;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::events::sendable_event_trait::SendableEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::subdirectory::trait_data_subdir::DataSubdir;
use crate::state::provider_priority::ProviderPriorityStore;
use crate::state::task_database::TaskDatabase;
use crate::utils::get_url_file_extension::get_url_file_extension;
use crate::utils::simple_http_download::simple_http_download;
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_client::endpoints::media_files::get_media_file::get_media_file;
use artcraft_client::error::storyteller_error::StorytellerError;
use artcraft_client::utils::api_host::ApiHost;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
use errors::AnyhowError;
use log::{error, info, warn};
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use serde_derive::{Deserialize, Serialize};
use sqlite_database::queries::create_task::{create_task, CreateTaskArgs};
use std::time::Duration;
use tauri::{AppHandle, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
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_2")]
NanoBanana2,
#[serde(rename = "nano_banana_pro")]
NanoBananaPro,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "gpt_image_1p5")]
GptImage1p5,
#[serde(rename = "seedream_4")]
Seedream4,
#[serde(rename = "seedream_4p5")]
Seedream4p5,
#[serde(rename = "seedream_5_lite")]
Seedream5Lite,
#[serde(rename = "qwen_edit_2511_angles")]
QwenEdit2511Angles,
#[serde(rename = "flux_2_lora_angles")]
Flux2LoraAngles,
// #[serde(rename = "qwen")]
// Qwen,
//
// #[serde(rename = "seededit_3")]
// SeedEdit3,
}
#[derive(Deserialize, Debug)]
pub struct EnqueueEditImageCommand {
/// The provider to use (defaults to Artcraft/Storyteller).
/// Not all (provider, model) combinations are valid.
pub provider: Option<GenerationProvider>,
/// The model to use.
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.
/// There must be at least one image.
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// If set, this becomes the first image in the image media tokens (pushing back
/// each of the `image_media_tokens` by one).
/// This is useful if we want to do prompt engineering.
pub scene_image_media_token: Option<MediaFileToken>,
/// The user's image generation prompt.
pub prompt: String,
/// Turn off the system prompt.
pub disable_system_prompt: Option<bool>,
/// Number of images to generate.
pub image_count: Option<u32>,
/// Aspect ratio.
#[deprecated(note="use common_aspect_ratio")]
pub aspect_ratio: Option<EditImageSize>,
/// New field for aspect ratio.
/// Not all models support each of these aspect ratios, but we can choose or interpolate sensibly.
pub common_aspect_ratio: Option<CommonAspectRatio>,
/// Image quality.
pub image_quality: Option<EditImageQuality>,
/// Image resolution
pub image_resolution: Option<EditImageResolution>,
/// Quality.
/// Tends to be used by OpenAI models.
pub quality: Option<CommonQuality>,
/// OPTIONAL.
/// Only for image angle edit models.
pub horizontal_angle: Option<f64>,
/// OPTIONAL.
/// Only for image angle edit models.
pub vertical_angle: Option<f64>,
/// OPTIONAL.
/// Only for image angle edit models.
pub zoom: Option<f64>,
/// OPTIONAL.
/// Name of the frontend caller.
/// We'll use this to selectively trigger events.
pub frontend_caller: Option<TauriCommandCaller>,
/// OPTIONAL.
/// A frontend-defined identifier that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_id: Option<String>,
/// OPTIONAL.
/// A frontend-defined payload that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_payload: Option<String>,
}
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EditImageSize {
Auto,
Square,
Wide,
Tall,
}
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EditImageQuality {
Auto,
High,
Medium,
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 EnqueueEditImageErrorType {
/// Caller didn't specify a model
ModelNotSpecified,
/// No model available for image generation
NoProviderAvailable,
/// Generic bad request error
BadRequest,
/// Generic server error
ServerError,
/// The user is sending too many requests
TooManyConcurrentTasks,
/// User is not logged into Sora!
SoraLoginRequired,
/// The user needs to create a Sora account
SoraUsernameNotYetCreated,
/// The Sora service is having problems. Try again soon.
SoraIsHavingProblems,
}
#[derive(Serialize)]
pub struct EnqueueEditImageSuccessResponse {
}
impl SerializeMarker for EnqueueEditImageSuccessResponse {}
#[tauri::command]
pub async fn enqueue_edit_image_command(
app: AppHandle,
request: EnqueueEditImageCommand,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
sora_creds_manager: State<'_, SoraCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
) -> ResponseOrErrorType<EnqueueEditImageSuccessResponse, EnqueueEditImageErrorType> {
info!("enqueue_edit_image_command called; image media tokens : {:?}, full request: {:?}",
&request.image_media_tokens, &request);
let result = handle_request(
&request,
&app,
&app_data_root,
&app_env_configs,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
&sora_creds_manager,
&sora_task_queue,
).await;
match result {
Err(err) => {
error!("Error enqueuing contextual edit image: {:?}", err);
notify_frontend_of_errors(&app, &err).await;
// TODO: Derive from err. Make service provider optional.
let event = GenerationEnqueueFailureEvent {
action: GenerationAction::GenerateImage,
service: GenerationServiceProvider::Artcraft, // FIXME: This is wrong.
model: None,
reason: None,
};
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
Err(error_to_tauri_response(err))
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
action: event.to_frontend_event_action(),
service: event.to_frontend_event_service(),
model: event.model,
};
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
CreditsBalanceChangedEvent{}.send_infallible(&app);
Ok(EnqueueEditImageSuccessResponse {}.into())
}
}
}
pub async fn handle_request(
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let model = match request.model {
Some(model) => model,
None => {
return Err(GenerateError::no_model_specified())
}
};
let provider = request.provider
.unwrap_or(GenerationProvider::Artcraft);
info!("edit image with {:?} via provider {:?}", &model, &provider);
let mut success_event = match provider {
GenerationProvider::Artcraft => {
handle_image_edit_artcraft(
model,
request,
app,
app_data_root,
app_env_configs,
storyteller_creds_manager,
).await?
}
GenerationProvider::Sora => {
handle_image_edit_sora(
model,
request,
app,
app_data_root,
app_env_configs,
sora_creds_manager,
sora_task_queue,
).await?
}
_ => {
return Err(GenerateError::BadProviderForModel {
provider,
model: image_edit_model_to_model_type(model),
})
}
};
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,
request.frontend_caller.clone(),
request.frontend_subscriber_id.as_deref(),
request.frontend_subscriber_payload.as_deref(),
)
.await;
if let Err(err) = result {
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
// TODO: We don't know that this is strictly the edit page. It could be the stage. It might also be nice to distinguish inpainting.
let num_images = request.image_count.map(|count| count as u16).unwrap_or(1);
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::EditPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
fn error_to_tauri_response(error: GenerateError) -> CommandErrorResponseWrapper<EnqueueEditImageErrorType, ()> {
let mut status = CommandErrorStatus::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 = EnqueueEditImageErrorType::ModelNotSpecified;
error_message = "No model specified for image generation".to_string();
}
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
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 = 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 = EnqueueEditImageErrorType::BadRequest;
error_message = format!("Invalid number of input images ({}). Must be between {} and {}", provided, min, max);
}
_ => {} // Fall-through for now
}
CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
}
}

View File

@ -1,19 +0,0 @@
use enums::common::generation::common_model_type::CommonModelType;
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::ImageEditModel;
pub fn image_edit_model_to_model_type(model: ImageEditModel) -> CommonModelType {
match model {
ImageEditModel::FluxProKontextMax => CommonModelType::FluxProKontextMax,
ImageEditModel::Gemini25Flash => CommonModelType::NanoBanana,
ImageEditModel::NanoBanana => CommonModelType::NanoBanana,
ImageEditModel::NanoBanana2 => CommonModelType::NanoBanana2,
ImageEditModel::NanoBananaPro => CommonModelType::NanoBananaPro,
ImageEditModel::GptImage1 => CommonModelType::GptImage1,
ImageEditModel::GptImage1p5 => CommonModelType::GptImage1p5,
ImageEditModel::Seedream4 => CommonModelType::Seedream4,
ImageEditModel::Seedream4p5 => CommonModelType::Seedream4p5,
ImageEditModel::Seedream5Lite => CommonModelType::Seedream5Lite,
ImageEditModel::QwenEdit2511Angles => CommonModelType::QwenEdit2511Angles,
ImageEditModel::Flux2LoraAngles => CommonModelType::Flux2LoraAngles,
}
}

View File

@ -1,5 +0,0 @@
pub (super) mod artcraft;
pub (super) mod sora;
pub mod enqueue_edit_image_command;
pub mod image_edit_models;

View File

@ -1,40 +0,0 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{EnqueueEditImageCommand, ImageEditModel};
use crate::commands::deprecated::image_edit::image_edit_models::image_edit_model_to_model_type;
use crate::commands::deprecated::image_edit::sora::handle_sora_gpt_image_1_edit::handle_sora_gpt_image_1_edit;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use enums::common::generation_provider::GenerationProvider;
use tauri::AppHandle;
pub async fn handle_image_edit_sora(
model: ImageEditModel,
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
match model {
ImageEditModel::GptImage1 => {
handle_sora_gpt_image_1_edit(
request,
app,
app_data_root,
app_env_configs,
sora_creds_manager,
sora_task_queue,
).await
},
_ => {
Err(GenerateError::BadProviderForModel {
provider: GenerationProvider::Sora,
model: image_edit_model_to_model_type(model),
})
}
}
}

View File

@ -1,183 +0,0 @@
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageSize, EnqueueEditImageCommand, EnqueueEditImageErrorType};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::subdirectory::trait_data_subdir::DataSubdir;
use crate::state::provider_priority::ProviderPriorityStore;
use crate::utils::get_url_file_extension::get_url_file_extension;
use crate::utils::simple_http_download::simple_http_download;
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 enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use errors::AnyhowResult;
use uuid_utils::uuid::generate_random_uuid;
use log::{error, info, warn};
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use std::time::Duration;
use artcraft_client::endpoints::media_files::get_media_file::get_media_file;
use tauri::AppHandle;
const MAX_IMAGES: usize = 4;
const SORA_IMAGE_UPLOAD_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds
const SORA_IMAGE_REMIX_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds
const DEFAULT_ASPECT_RATIO : EditImageSize = EditImageSize::Square;
pub async fn handle_sora_gpt_image_1_edit(
request: &EnqueueEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let mut creds = match sora_creds_manager.get_credentials() {
Ok(Some(creds)) => creds,
Ok(None) => {
warn!("No Sora credentials found.");
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Sora, &app);
return Err(GenerateError::needs_sora_credentials());
}
Err(err) => {
error!("Failed to get Sora credentials: {:?}", err);
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Sora, &app);
return Err(GenerateError::needs_sora_credentials());
}
};
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,
}));
}
info!("Calling get media file API: {:?}", app_env_configs.storyteller_host);
let mut files_to_upload_to_sora = Vec::with_capacity(10);
// TODO(bt,2025-07-07): This is inefficient. Cache and parallelize this.
for media_token in media_tokens.iter() {
info!("Using media token: {:?}", media_token);
let response = get_media_file(
&app_env_configs.storyteller_host,
media_token
).await?;
let media_file_url = &response.media_file.media_links.cdn_url;
let extension_with_dot = get_url_file_extension(media_file_url)
.map(|ext| format!(".{}", ext))
.unwrap_or_else(|| ".png".to_string());
let filename = format!("{}{}", response.media_file.token.as_str(), extension_with_dot);
let filename = app_data_root.downloads_dir().path().join(&filename);
simple_http_download(&media_file_url, &filename).await?;
files_to_upload_to_sora.push(filename);
}
let credential_updated = maybe_upgrade_or_renew_session(&mut creds)
.await
.map_err(|err| {
error!("Failed to upgrade or renew session: {:?}", err);
err
})?;
if credential_updated {
info!("Storing updated credentials");
sora_creds_manager.set_credentials(&creds)?;
}
let mut sora_media_tokens = Vec::with_capacity(files_to_upload_to_sora.len());
for (i, file_path) in files_to_upload_to_sora.iter().enumerate() {
info!("Uploading image {} of {}...", (i+1), files_to_upload_to_sora.len());
let (response, maybe_new_credentials) =
image_upload_from_file_with_session_auto_renew(ImageUploadFromFileAutoRenewRequest {
file_path,
credentials: &creds,
request_timeout: Some(SORA_IMAGE_UPLOAD_TIMEOUT), // TODO: Centralize and make configurable.
}).await?;
if let Some(new_creds) = maybe_new_credentials {
info!("Storing updated credentials.");
sora_creds_manager.set_credentials(&new_creds)?;
creds = new_creds;
}
sora_media_tokens.push(response.id);
}
let aspect_ratio = request.aspect_ratio.unwrap_or(DEFAULT_ASPECT_RATIO);
let aspect_ratio = match aspect_ratio {
EditImageSize::Auto => ImageSize::Square,
EditImageSize::Square => ImageSize::Square,
EditImageSize::Wide => ImageSize::Wide,
EditImageSize::Tall => ImageSize::Tall,
};
info!("Calling Sora image generation...");
// TODO(bt,2025-04-21): Download media tokens.
// Note: This is incredibly inefficient. We should keep a local cache.
// Also, if they've already been uploaded to OpenAI, we shouldn't continue to re-upload.
let (response, maybe_new_creds) =
image_remix_with_session_auto_renew(ImageRemixAutoRenewRequest {
prompt: request.prompt.to_string(),
num_images: NumImages::One,
image_size: aspect_ratio,
sora_media_tokens: sora_media_tokens.clone(),
credentials: &creds,
request_timeout: Some(SORA_IMAGE_REMIX_TIMEOUT),
}).await?;
if let Some(new_creds) = maybe_new_creds {
info!("Storing updated credentials.");
sora_creds_manager.set_credentials(&new_creds)?;
}
info!("New Sora Task ID: {:?} ", response.task_id);
sora_task_queue.insert(&response.task_id)?;
Ok(TaskEnqueueSuccess {
task_type: TaskType::ImageGeneration,
model: Some(GenerationModel::GptImage1),
provider: GenerationProvider::Sora,
provider_job_id: Some(response.task_id.to_string()),
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}

View File

@ -1,2 +0,0 @@
mod handle_sora_gpt_image_1_edit;
pub (super) mod handle_image_edit_sora;

View File

@ -1,161 +0,0 @@
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::provider_priority::ProviderPriorityStore;
use crate::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
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_client::api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
use artcraft_client::api_defs::generate::image::inpaint::flux_dev_juggernaut_inpaint_image::{FluxDevJuggernautInpaintImageNumImages, FluxDevJuggernautInpaintImageRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use uuid_utils::uuid::generate_random_uuid;
use images::encoding::image_bytes_to_png_bytes::image_bytes_to_png_bytes;
use images::encoding::image_bytes_to_png_bytes_with_dimensions::image_bytes_to_png_bytes_with_dimensions;
use images::mask_images::normalize_image_bytes_to_flux_mask::normalize_image_bytes_to_flux_mask;
use log::{error, info};
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_client::endpoints::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
use artcraft_client::endpoints::generate::image::inpaint::flux_dev_juggernaut_inpaint_image::flux_dev_juggernaut_inpaint_image;
use artcraft_client::endpoints::generate::image::inpaint::flux_pro_1_inpaint_image::flux_pro_1_inpaint_image;
use artcraft_client::endpoints::media_files::get_media_file::get_media_file;
use artcraft_client::endpoints::media_files::upload_image_media_file_from_bytes::{upload_image_media_file_from_bytes, ImageType, UploadImageBytesArgs};
use artcraft_client::endpoints::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub async fn handle_artcraft_flux_dev_juggernaut_inpaint(
request: &EnqueueInpaintImageCommand,
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());
},
};
let image_media_token = match &request.image_media_token {
Some(token) => token.clone(),
None => {
return Err(GenerateError::required_source_image_not_provided());
},
};
let mask_media_token = get_mask(
request,
app_env_configs,
&creds,
).await?;
info!("Calling Artcraft flux dev juggernaut inpaint ...");
let uuid_idempotency_token = generate_random_uuid();
let num_images = match request.image_count {
None => None,
Some(1) => Some(FluxDevJuggernautInpaintImageNumImages::One),
Some(2) => Some(FluxDevJuggernautInpaintImageNumImages::Two),
Some(3) => Some(FluxDevJuggernautInpaintImageNumImages::Three),
Some(4) => Some(FluxDevJuggernautInpaintImageNumImages::Four),
Some(other) => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
}));
},
};
let request = FluxDevJuggernautInpaintImageRequest {
uuid_idempotency_token,
prompt: Some(request.prompt.clone()),
image_media_token,
mask_media_token,
num_images,
};
let result = flux_dev_juggernaut_inpaint_image(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft flux dev juggernaut inpaint. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft flux dev juggernaut inpaint: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::FluxDevJuggernaut),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageInpaintEdit,
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}
async fn get_mask(
request: &EnqueueInpaintImageCommand,
app_env_configs: &AppEnvConfigs,
storyteller_creds: &StorytellerCredentialSet,
) -> Result<MediaFileToken, GenerateError> {
if request.mask_image_media_token.is_some() && request.mask_image_raw_bytes.is_some() {
return Err(GenerateError::both_image_mask_media_token_and_bytes_supplied());
}
if let Some(token) = request.mask_image_media_token.as_ref() {
return Ok(token.clone());
};
let image_bytes = request.mask_image_raw_bytes.as_ref()
.ok_or(GenerateError::required_source_image_mask_not_provided())?;
let image_bytes = normalize_image_bytes_to_flux_mask(image_bytes)
.map_err(|err| {
error!("Failed to convert image bytes to png: {:?}", err);
GenerateError::AnyhowError(anyhow!("Failed to convert image bytes to png mask"))
})?;
info!("Uploading image media file from bytes...");
let result = upload_image_media_file_from_bytes(UploadImageBytesArgs {
api_host: &app_env_configs.storyteller_host,
maybe_creds: Some(&storyteller_creds),
image_bytes: image_bytes.0,
image_type: ImageType::Png,
is_intermediate_system_file: true,
maybe_generation_provider: None,
}).await
.map_err(|err| {
error!("Failed to upload image media file: {:?}", err);
GenerateError::from(err)
})?;
Ok(result.media_file_token)
}

View File

@ -1,160 +0,0 @@
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::provider_priority::ProviderPriorityStore;
use crate::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
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_client::api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
use artcraft_client::api_defs::generate::image::inpaint::flux_pro_1_inpaint_image::{FluxPro1InpaintImageNumImages, FluxPro1InpaintImageRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use uuid_utils::uuid::generate_random_uuid;
use images::encoding::image_bytes_to_png_bytes::image_bytes_to_png_bytes;
use images::encoding::image_bytes_to_png_bytes_with_dimensions::image_bytes_to_png_bytes_with_dimensions;
use images::mask_images::normalize_image_bytes_to_flux_mask::normalize_image_bytes_to_flux_mask;
use log::{error, info};
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_client::endpoints::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
use artcraft_client::endpoints::generate::image::inpaint::flux_pro_1_inpaint_image::flux_pro_1_inpaint_image;
use artcraft_client::endpoints::media_files::get_media_file::get_media_file;
use artcraft_client::endpoints::media_files::upload_image_media_file_from_bytes::{upload_image_media_file_from_bytes, ImageType, UploadImageBytesArgs};
use artcraft_client::endpoints::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub async fn handle_artcraft_flux_pro_1_inpaint(
request: &EnqueueInpaintImageCommand,
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());
},
};
let image_media_token = match &request.image_media_token {
Some(token) => token.clone(),
None => {
return Err(GenerateError::required_source_image_not_provided());
},
};
let mask_media_token = get_mask(
request,
app_env_configs,
&creds,
).await?;
info!("Calling Artcraft flux pro 1 inpaint ...");
let uuid_idempotency_token = generate_random_uuid();
let num_images = match request.image_count {
None => None,
Some(1) => Some(FluxPro1InpaintImageNumImages::One),
Some(2) => Some(FluxPro1InpaintImageNumImages::Two),
Some(3) => Some(FluxPro1InpaintImageNumImages::Three),
Some(4) => Some(FluxPro1InpaintImageNumImages::Four),
Some(other) => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
}));
},
};
let request = FluxPro1InpaintImageRequest {
uuid_idempotency_token,
prompt: Some(request.prompt.clone()),
image_media_token,
mask_media_token,
num_images,
};
let result = flux_pro_1_inpaint_image(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft flux pro 1 inpaint. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft flux pro 1 inpaint: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::FluxPro1),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageInpaintEdit,
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}
async fn get_mask(
request: &EnqueueInpaintImageCommand,
app_env_configs: &AppEnvConfigs,
storyteller_creds: &StorytellerCredentialSet,
) -> Result<MediaFileToken, GenerateError> {
if request.mask_image_media_token.is_some() && request.mask_image_raw_bytes.is_some() {
return Err(GenerateError::both_image_mask_media_token_and_bytes_supplied());
}
if let Some(token) = request.mask_image_media_token.as_ref() {
return Ok(token.clone());
};
let image_bytes = request.mask_image_raw_bytes.as_ref()
.ok_or(GenerateError::required_source_image_mask_not_provided())?;
let image_bytes = normalize_image_bytes_to_flux_mask(image_bytes)
.map_err(|err| {
error!("Failed to convert image bytes to png: {:?}", err);
GenerateError::AnyhowError(anyhow!("could not encode image mask to png"))
})?;
info!("Uploading image media file from bytes...");
let result = upload_image_media_file_from_bytes(UploadImageBytesArgs {
api_host: &app_env_configs.storyteller_host,
maybe_creds: Some(&storyteller_creds),
image_bytes: image_bytes.0,
image_type: ImageType::Png,
is_intermediate_system_file: true,
maybe_generation_provider: None,
}).await
.map_err(|err| {
error!("Failed to upload image media file: {:?}", err);
GenerateError::from(err)
})?;
Ok(result.media_file_token)
}

View File

@ -1,132 +0,0 @@
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::provider_priority::ProviderPriorityStore;
use crate::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
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_client::api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{FluxProKontextMaxEditImageNumImages, FluxProKontextMaxEditImageRequest};
use artcraft_client::api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
use artcraft_client::api_defs::generate::image::inpaint::flux_pro_1_inpaint_image::{FluxPro1InpaintImageNumImages, FluxPro1InpaintImageRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use uuid_utils::uuid::generate_random_uuid;
use images::encoding::image_bytes_to_png_bytes::image_bytes_to_png_bytes;
use images::encoding::image_bytes_to_png_bytes_with_dimensions::image_bytes_to_png_bytes_with_dimensions;
use images::mask_images::normalize_image_bytes_to_flux_mask::normalize_image_bytes_to_flux_mask;
use log::{error, info};
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
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::gpt_image_1_edit_image::gpt_image_1_edit_image;
use artcraft_client::endpoints::generate::image::inpaint::flux_pro_1_inpaint_image::flux_pro_1_inpaint_image;
use artcraft_client::endpoints::media_files::get_media_file::get_media_file;
use artcraft_client::endpoints::media_files::upload_image_media_file_from_bytes::{upload_image_media_file_from_bytes, ImageType, UploadImageBytesArgs};
use artcraft_client::endpoints::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub async fn handle_artcraft_flux_pro_kontext_inpaint(
request: &EnqueueInpaintImageCommand,
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());
},
};
let image_media_token = match &request.image_media_token {
Some(token) => token.clone(),
None => {
return Err(GenerateError::required_source_image_not_provided());
},
};
//let image_media_file = get_media_file(
// &app_env_configs.storyteller_host,
// &image_media_token,
//).await.map_err(|err| {
// error!("Failed to get media file for image: {:?}", err);
// InternalImageInpaintError::StorytellerError(err)
//})?;
// TODO: Experiment in seeing if we can use the mask in any useful way.
// let mask_media_token = get_mask(
// request,
// app_env_configs,
// &creds,
// ).await?;
info!("Calling Artcraft flux pro kontext inpaint ...");
let uuid_idempotency_token = generate_random_uuid();
let num_images = match request.image_count {
None => None,
Some(1) => Some(FluxProKontextMaxEditImageNumImages::One),
Some(2) => Some(FluxProKontextMaxEditImageNumImages::Two),
Some(3) => Some(FluxProKontextMaxEditImageNumImages::Three),
Some(4) => Some(FluxProKontextMaxEditImageNumImages::Four),
Some(other) => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
}));
},
};
let request = FluxProKontextMaxEditImageRequest {
uuid_idempotency_token,
prompt: Some(request.prompt.clone()),
image_media_token,
num_images,
};
let result = flux_pro_kontext_max_edit_image(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft flux pro kontext inpaint. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft flux pro kontext inpaint: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::FluxProKontextMax),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageInpaintEdit,
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}

View File

@ -1,117 +0,0 @@
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
use crate::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::provider_priority::ProviderPriorityStore;
use crate::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
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_client::api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{FluxProKontextMaxEditImageNumImages, FluxProKontextMaxEditImageRequest};
use artcraft_client::api_defs::generate::image::edit::gemini_25_flash_edit_image::{Gemini25FlashEditImageNumImages, Gemini25FlashEditImageRequest};
use artcraft_client::api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
use artcraft_client::api_defs::generate::image::inpaint::flux_pro_1_inpaint_image::{FluxPro1InpaintImageNumImages, FluxPro1InpaintImageRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use uuid_utils::uuid::generate_random_uuid;
use images::encoding::image_bytes_to_png_bytes::image_bytes_to_png_bytes;
use images::encoding::image_bytes_to_png_bytes_with_dimensions::image_bytes_to_png_bytes_with_dimensions;
use images::mask_images::normalize_image_bytes_to_flux_mask::normalize_image_bytes_to_flux_mask;
use log::{error, info};
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
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::inpaint::flux_pro_1_inpaint_image::flux_pro_1_inpaint_image;
use artcraft_client::endpoints::media_files::get_media_file::get_media_file;
use artcraft_client::endpoints::media_files::upload_image_media_file_from_bytes::{upload_image_media_file_from_bytes, ImageType, UploadImageBytesArgs};
use artcraft_client::endpoints::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub async fn handle_artcraft_nano_banana_inpaint(
request: &EnqueueInpaintImageCommand,
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());
},
};
let image_media_token = match &request.image_media_token {
Some(token) => token.clone(),
None => {
return Err(GenerateError::required_source_image_not_provided());
},
};
info!("Calling Artcraft gemini 2.5 flash inpaint ...");
let uuid_idempotency_token = generate_random_uuid();
let num_images = match request.image_count {
None => None,
Some(1) => Some(Gemini25FlashEditImageNumImages::One),
Some(2) => Some(Gemini25FlashEditImageNumImages::Two),
Some(3) => Some(Gemini25FlashEditImageNumImages::Three),
Some(4) => Some(Gemini25FlashEditImageNumImages::Four),
Some(other) => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
}));
},
};
let request = Gemini25FlashEditImageRequest {
uuid_idempotency_token,
prompt: Some(request.prompt.clone()),
image_media_tokens: Some(vec![image_media_token]),
num_images,
image_quality: None,
};
let result = gemini_25_flash_edit_image(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft gemini 2.5 flash inpaint. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft gemini 2.5 flash inpaint: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::Gemini25Flash),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageInpaintEdit,
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}

View File

@ -1,60 +0,0 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::deprecated::image_inpaint::artcraft::handle_artcraft_flux_dev_juggernaut_inpaint::handle_artcraft_flux_dev_juggernaut_inpaint;
use crate::commands::deprecated::image_inpaint::artcraft::handle_artcraft_flux_pro_1_inpaint::handle_artcraft_flux_pro_1_inpaint;
use crate::commands::deprecated::image_inpaint::artcraft::handle_artcraft_flux_pro_kontext_inpaint::handle_artcraft_flux_pro_kontext_inpaint;
use crate::commands::deprecated::image_inpaint::artcraft::handle_artcraft_nano_banana_inpaint::handle_artcraft_nano_banana_inpaint;
use crate::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::{EnqueueInpaintImageCommand, ImageInpaintModel};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use tauri::AppHandle;
pub async fn handle_inpaint_artcraft(
model: ImageInpaintModel,
request: &EnqueueInpaintImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
match model {
ImageInpaintModel::FluxDevJuggernaut => {
handle_artcraft_flux_dev_juggernaut_inpaint(
request,
app,
app_data_root,
app_env_configs,
storyteller_creds_manager,
).await
}
ImageInpaintModel::FluxPro1 => {
handle_artcraft_flux_pro_1_inpaint(
request,
app,
app_data_root,
app_env_configs,
storyteller_creds_manager,
).await
}
ImageInpaintModel::FluxProKontextMax => {
handle_artcraft_flux_pro_kontext_inpaint(
request,
app,
app_data_root,
app_env_configs,
storyteller_creds_manager,
).await
}
ImageInpaintModel::Gemini25Flash => {
handle_artcraft_nano_banana_inpaint(
request,
app,
app_data_root,
app_env_configs,
storyteller_creds_manager,
).await
}
}
}

View File

@ -1,6 +0,0 @@
pub (super) mod handle_inpaint_artcraft;
mod handle_artcraft_flux_pro_1_inpaint;
mod handle_artcraft_flux_dev_juggernaut_inpaint;
mod handle_artcraft_flux_pro_kontext_inpaint;
mod handle_artcraft_nano_banana_inpaint;

View File

@ -1,348 +0,0 @@
use crate::api_adapters::quality::common_quality::CommonQuality;
use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::deprecated::image_edit::image_edit_models::image_edit_model_to_model_type;
use crate::commands::deprecated::image_inpaint::artcraft::handle_inpaint_artcraft::handle_inpaint_artcraft;
use crate::commands::deprecated::image_inpaint::inpaint_models::image_inpaint_model_to_model_type;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::commands::response::shorthand::{Response, ResponseOrErrorType};
use crate::commands::response::success_response_wrapper::SerializeMarker;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::events::sendable_event_trait::SendableEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::subdirectory::trait_data_subdir::DataSubdir;
use crate::state::provider_priority::ProviderPriorityStore;
use crate::state::task_database::TaskDatabase;
use crate::utils::get_url_file_extension::get_url_file_extension;
use crate::utils::simple_http_download::simple_http_download;
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_client::endpoints::media_files::get_media_file::get_media_file;
use artcraft_client::error::storyteller_error::StorytellerError;
use artcraft_client::utils::api_host::ApiHost;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
use errors::AnyhowError;
use log::{error, info, warn};
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use serde_derive::{Deserialize, Serialize};
use sqlite_database::queries::create_task::{create_task, CreateTaskArgs};
use std::time::Duration;
use tauri::{AppHandle, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum ImageInpaintModel {
/*
* Mask-based inpainting models
*/
#[serde(rename = "flux_dev_juggernaut")]
FluxDevJuggernaut,
#[serde(rename = "flux_pro_1")]
FluxPro1,
/*
* Non-inpainting, instructive editing models
* NB: We're supporting these to keep the app simple and not over-complicate the javascript
*/
#[serde(rename = "flux_pro_kontext_max")]
FluxProKontextMax,
#[serde(rename = "gemini_25_flash")]
Gemini25Flash,
}
#[derive(Deserialize, Debug)]
pub struct EnqueueInpaintImageCommand {
/// The provider to use (defaults to Artcraft/Storyteller).
/// Not all (provider, model) combinations are valid.
pub provider: Option<GenerationProvider>,
/// REQUIRED (Option<T> is just for error messages).
/// The model to use.
pub model: Option<ImageInpaintModel>,
/// REQUIRED (Option<T> is just for error messages).
/// The source image to edit.
pub image_media_token: Option<MediaFileToken>,
/// REQUIRED: Supply this *XOR* `mask_image_raw_bytes`.
/// The mask to focus the edit (already uploaded).
pub mask_image_media_token: Option<MediaFileToken>,
/// REQUIRED: Supply this *XOR* `mask_image_media_token`.
/// The mask to focus the edit (raw bytes).
pub mask_image_raw_bytes: Option<Vec<u8>>,
/// REQUIRED.
/// The user's image generation prompt.
pub prompt: String,
/// Number of images to generate.
pub image_count: Option<u32>,
/// Quality.
/// Tends to be used by OpenAI models.
pub quality: Option<CommonQuality>,
/// If true, force the dimensions of the source image and mask image to match.
pub require_matching_dimensions: Option<bool>,
/// OPTIONAL.
/// Name of the frontend caller.
/// We'll use this to selectively trigger events.
pub frontend_caller: Option<TauriCommandCaller>,
/// OPTIONAL.
/// A frontend-defined identifier that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_id: Option<String>,
/// OPTIONAL.
/// A frontend-defined payload that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_payload: Option<String>,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueInpaintImageErrorType {
/// Caller didn't specify a model
ModelNotSpecified,
/// No model available for image generation
NoProviderAvailable,
/// No source image was supplied.
NoSourceImageSpecified,
/// No mask image was supplied.
NoMaskImageSpecified,
/// Too many mask images were supplied.
MultipleMaskImagesSpecified,
/// Bad mask image was supplied.
BadMaskImage,
/// Generic bad request error
BadRequest,
/// Generic server error
ServerError,
}
#[derive(Serialize)]
pub struct EnqueueImageInpaintSuccessResponse {
}
impl SerializeMarker for EnqueueImageInpaintSuccessResponse {}
#[tauri::command]
pub async fn enqueue_image_inpaint_command(
app: AppHandle,
request: EnqueueInpaintImageCommand,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
sora_creds_manager: State<'_, SoraCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
) -> ResponseOrErrorType<EnqueueImageInpaintSuccessResponse, EnqueueInpaintImageErrorType> {
info!("enqueue_image_inpaint_command called; model: {:?}, image_media_token: {:?}, prompt: {:?}, image_count: {:?}",
&request.model, &request.image_media_token, &request.prompt, &request.image_count);
let result = handle_request(
&request,
&app,
&app_data_root,
&app_env_configs,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
&sora_creds_manager,
&sora_task_queue,
).await;
match result {
Err(err) => {
error!("Error enqueuing inpaint image: {:?}", err);
notify_frontend_of_errors(&app, &err).await;
// TODO: Derive from err. Make service provider optional.
let event = GenerationEnqueueFailureEvent {
action: GenerationAction::GenerateImage,
service: GenerationServiceProvider::Artcraft, // FIXME: This is wrong.
model: None,
reason: None,
};
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
Err(error_to_tauri_response(err))
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
action: event.to_frontend_event_action(),
service: event.to_frontend_event_service(),
model: event.model,
};
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
CreditsBalanceChangedEvent{}.send_infallible(&app);
Ok(EnqueueImageInpaintSuccessResponse {}.into())
}
}
}
pub async fn handle_request(
request: &EnqueueInpaintImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let model = match request.model {
Some(model) => model,
None => {
return Err(GenerateError::no_model_specified())
}
};
let provider = request.provider
.unwrap_or(GenerationProvider::Artcraft);
info!("image inpaint with {:?} via provider {:?}", &model, &provider);
let success_event = match provider {
GenerationProvider::Artcraft => {
handle_inpaint_artcraft(
model,
request,
app,
app_data_root,
app_env_configs,
storyteller_creds_manager,
).await?
}
_ => {
return Err(GenerateError::BadProviderForModel {
provider,
model: image_inpaint_model_to_model_type(model),
})
}
};
let result = success_event
.insert_into_task_database_with_frontend_payload(
task_database,
request.frontend_caller,
request.frontend_subscriber_id.as_deref(),
request.frontend_subscriber_payload.as_deref()
)
.await;
if let Err(err) = result {
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
// TODO: We don't know that this is strictly the edit page. It could be the stage. It might also be nice to distinguish inpainting.
let num_images = request.image_count.map(|count| count as u16).unwrap_or(1);
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::EditPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
pub fn error_to_tauri_response(error: GenerateError) -> CommandErrorResponseWrapper<EnqueueInpaintImageErrorType, ()> {
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueInpaintImageErrorType::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 = EnqueueInpaintImageErrorType::ModelNotSpecified;
error_message = "No model specified for image generation".to_string();
}
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueInpaintImageErrorType::NoProviderAvailable;
error_message = "No configured provider available for image generation".to_string();
}
GenerateError::BadInput(BadInputReason::RequiredSourceImageNotProvided) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::NoSourceImageSpecified;
error_message = "No source image was provided".to_string();
}
GenerateError::BadInput(BadInputReason::RequiredSourceImageMaskNotProvided) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::NoMaskImageSpecified;
error_message = "No mask image was provided".to_string();
}
GenerateError::BadInput(BadInputReason::BothImageMaskMediaTokenAndBytesSupplied) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::MultipleMaskImagesSpecified;
error_message = "multiple mask images provided".to_string();
}
GenerateError::BadInput(BadInputReason::CannotDetermineImageMimeType) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::BadMaskImage;
error_message = "bad mask image (mime)".to_string();
}
GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages { min, max, requested }) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::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 = EnqueueInpaintImageErrorType::BadRequest;
error_message = format!("Invalid number of input images ({}). Must be between {} and {}", provided, min, max);
}
_ => {}, // Other cases fall through.
}
CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
}
}

View File

@ -1,11 +0,0 @@
use crate::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::ImageInpaintModel;
use enums::common::generation::common_model_type::CommonModelType;
pub fn image_inpaint_model_to_model_type(model: ImageInpaintModel) -> CommonModelType {
match model {
ImageInpaintModel::FluxDevJuggernaut => CommonModelType::FluxDevJuggernaut,
ImageInpaintModel::FluxPro1 => CommonModelType::FluxPro1,
ImageInpaintModel::FluxProKontextMax => CommonModelType::FluxProKontextMax,
ImageInpaintModel::Gemini25Flash => CommonModelType::NanoBanana,
}
}

View File

@ -1,4 +0,0 @@
pub (super) mod inpaint_models;
pub mod enqueue_image_inpaint_command;
mod artcraft;

View File

@ -1,3 +0,0 @@
pub mod image_edit;
pub mod image_inpaint;
pub mod text_to_image;

View File

@ -1,115 +0,0 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::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_client::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 uuid_utils::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::gpt_image_1_edit_image::gpt_image_1_edit_image;
use artcraft_client::endpoints::generate::image::text::generate_gpt_image_1_text_to_image::generate_gpt_image_1_text_to_image;
use tauri::AppHandle;
pub async fn handle_artcraft_gpt_image_1_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::needs_storyteller_credentials());
},
};
info!("Calling Artcraft gpt-image-1 ...");
let uuid_idempotency_token = generate_random_uuid();
let image_quality = Some(GenerateGptImage1TextToImageImageQuality::High);
//let image_quality = request.image_quality
// .map(|quality| match quality {
// EditImageQuality::Auto => GenerateGptImage1TextToImageImageQuality::Auto,
// EditImageQuality::High => GenerateGptImage1TextToImageImageQuality::High,
// EditImageQuality::Medium => GenerateGptImage1TextToImageImageQuality::Medium,
// EditImageQuality::Low => GenerateGptImage1TextToImageImageQuality::Low,
// });
let image_size = request.aspect_ratio
.map(|size| match size {
TextToImageSize::Auto => GenerateGptImage1TextToImageImageSize::Square,
TextToImageSize::Square => GenerateGptImage1TextToImageImageSize::Square,
TextToImageSize::Tall => GenerateGptImage1TextToImageImageSize::Vertical,
TextToImageSize::Wide => GenerateGptImage1TextToImageImageSize::Horizontal,
});
let num_images = match request.number_images {
None => None,
Some(1) => Some(GenerateGptImage1TextToImageNumImages::One),
Some(2) => Some(GenerateGptImage1TextToImageNumImages::Two),
Some(3) => Some(GenerateGptImage1TextToImageNumImages::Three),
Some(4) => Some(GenerateGptImage1TextToImageNumImages::Four),
// TODO: Error
//Some(other) => {
// return Err(InternalImageError::InvalidNumberOfRequestedImages {
// min: 1,
// max: 4,
// requested: other,
// });
//},
_ => Some(GenerateGptImage1TextToImageNumImages::One), // Default to one image if invalid number
};
let request = GenerateGptImage1TextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
image_size,
num_images,
image_quality,
};
let result = generate_gpt_image_1_text_to_image(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft gpt-image-1. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::GptImage1),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}

View File

@ -1,50 +0,0 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::deprecated::text_to_image::artcraft::handle_artcraft_gpt_image_1_text_to_image::handle_artcraft_gpt_image_1_text_to_image;
use crate::commands::deprecated::text_to_image::artcraft::handle_text_to_image_artcraft_via_router::handle_text_to_image_artcraft_via_router;
use crate::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageModel};
use crate::commands::deprecated::text_to_image::text_to_image_models::text_to_image_model_to_model_type;
use crate::events::generation_events::common::GenerationModel;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use router::api::router_image_model::RouterImageModel;
use enums::common::generation_provider::GenerationProvider;
use tauri::AppHandle;
pub async fn handle_text_to_image_artcraft(
model: TextToImageModel,
request: &EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
match model {
TextToImageModel::Midjourney | TextToImageModel::GrokImage => {
Err(GenerateError::BadProviderForModel {
provider: GenerationProvider::Artcraft,
model: text_to_image_model_to_model_type(model),
})
}
TextToImageModel::Recraft3 => {
Err(GenerateError::NotYetImplemented(format!("not yet implemented in Artcraft")))
}
TextToImageModel::Flux1Dev => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::Flux1Dev, GenerationModel::Flux1Dev).await,
TextToImageModel::Flux1Schnell => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::Flux1Schnell, GenerationModel::Flux1Schnell).await,
TextToImageModel::FluxPro11 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::FluxPro11, GenerationModel::FluxPro11).await,
TextToImageModel::FluxPro11Ultra => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::FluxPro11Ultra, GenerationModel::FluxPro11Ultra).await,
TextToImageModel::GptImage1 => handle_artcraft_gpt_image_1_text_to_image(request, app_env_configs, storyteller_creds_manager).await,
TextToImageModel::GptImage1p5 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::GptImage1p5, GenerationModel::GptImage1p5).await,
TextToImageModel::GptImage2 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::GptImage2, GenerationModel::GptImage2).await,
TextToImageModel::Gemini25Flash | TextToImageModel::NanoBanana => {
handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::NanoBanana, GenerationModel::NanoBanana).await
},
TextToImageModel::NanoBanana2 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::NanoBanana2, GenerationModel::NanoBanana2).await,
TextToImageModel::NanoBananaPro => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::NanoBananaPro, GenerationModel::NanoBananaPro).await,
TextToImageModel::Seedream4 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::Seedream4, GenerationModel::Seedream4).await,
TextToImageModel::Seedream4p5 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::Seedream4p5, GenerationModel::Seedream4p5).await,
TextToImageModel::Seedream5Lite => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, RouterImageModel::Seedream5Lite, GenerationModel::Seedream5Lite).await,
}
}

View File

@ -1,173 +0,0 @@
use crate::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio as CommonAspectRatio2;
use crate::api_adapters::resolution::common_resolution::CommonResolution as CommonResolution2;
use crate::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::deprecated::text_to_image::enqueue_text_to_image_command::{
EnqueueTextToImageRequest, TextToImageResolution, TextToImageSize,
};
use crate::events::generation_events::common::GenerationModel;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use router::api::router_aspect_ratio::RouterAspectRatio;
use router::api::router_image_model::RouterImageModel;
use router::api::router_resolution::RouterResolution;
use router::api::router_quality::RouterQuality;
use router::api::image_list_ref::ImageListRef;
use router::api::router_provider::RouterProvider;
use router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use router::client::router_artcraft_client::RouterArtcraftClient;
use router::client::router_client::RouterClient;
use router::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use router::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
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: RouterImageModel,
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.clone().map(ImageListRef::MediaFileTokens);
let aspect_ratio = get_aspect_ratio_t2i(request);
let resolution = get_resolution_t2i(request);
let quality = get_quality_t2i(request);
let router_request = GenerateImageRequestBuilder {
model,
provider: RouterProvider::Artcraft,
prompt: request.prompt.clone(),
image_inputs,
resolution,
aspect_ratio,
quality,
image_batch_count: request.number_images.map(|n| n as u16),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
let dor = router_request.build2()?;
info!("Image Generation Request: {:?}", dor);
let request = match dor {
ImageGenerationDraftOrRequest::Request(req) => req,
// Artcraft never returns a Draft — image-token resolution happens server-side.
// The only draft-producing models today are Kinovi-Midjourney variants,
// which are routed to a separate provider.
ImageGenerationDraftOrRequest::Draft(_) => unreachable!(
"Artcraft router should never produce a draft for text-to-image"
),
};
let response = match request.send_request(&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),
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}
fn get_aspect_ratio_t2i(request: &EnqueueTextToImageRequest) -> Option<RouterAspectRatio> {
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 => RouterAspectRatio::Auto,
TextToImageSize::Square => RouterAspectRatio::Square,
TextToImageSize::Wide => RouterAspectRatio::Wide,
TextToImageSize::Tall => RouterAspectRatio::Tall,
});
}
None
}
fn get_resolution_t2i(request: &EnqueueTextToImageRequest) -> Option<RouterResolution> {
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 => RouterResolution::OneK,
TextToImageResolution::TwoK => RouterResolution::TwoK,
TextToImageResolution::FourK => RouterResolution::FourK,
});
}
None
}
fn get_quality_t2i(request: &EnqueueTextToImageRequest) -> Option<RouterQuality> {
request.quality.map(|quality| quality.to_artcraft_router_type())
}
fn convert_desktop_aspect_ratio(ar: CommonAspectRatio2) -> RouterAspectRatio {
match ar {
CommonAspectRatio2::Auto => RouterAspectRatio::Auto,
CommonAspectRatio2::Square => RouterAspectRatio::Square,
CommonAspectRatio2::WideThreeByTwo => RouterAspectRatio::WideThreeByTwo,
CommonAspectRatio2::WideFourByThree => RouterAspectRatio::WideFourByThree,
CommonAspectRatio2::WideFiveByFour => RouterAspectRatio::WideFiveByFour,
CommonAspectRatio2::WideSixteenByNine => RouterAspectRatio::WideSixteenByNine,
CommonAspectRatio2::WideTwentyOneByNine => RouterAspectRatio::WideTwentyOneByNine,
CommonAspectRatio2::TallTwoByThree => RouterAspectRatio::TallTwoByThree,
CommonAspectRatio2::TallThreeByFour => RouterAspectRatio::TallThreeByFour,
CommonAspectRatio2::TallFourByFive => RouterAspectRatio::TallFourByFive,
CommonAspectRatio2::TallNineBySixteen => RouterAspectRatio::TallNineBySixteen,
CommonAspectRatio2::TallNineByTwentyOne => RouterAspectRatio::TallNineByTwentyOne,
CommonAspectRatio2::Wide => RouterAspectRatio::Wide,
CommonAspectRatio2::Tall => RouterAspectRatio::Tall,
CommonAspectRatio2::Auto2k => RouterAspectRatio::Auto2k,
CommonAspectRatio2::Auto4k => RouterAspectRatio::Auto4k,
CommonAspectRatio2::SquareHd => RouterAspectRatio::SquareHd,
}
}
fn convert_desktop_resolution(res: CommonResolution2) -> RouterResolution {
match res {
CommonResolution2::OneK => RouterResolution::OneK,
CommonResolution2::TwoK => RouterResolution::TwoK,
CommonResolution2::ThreeK => RouterResolution::ThreeK,
CommonResolution2::FourK => RouterResolution::FourK,
CommonResolution2::HalfK => RouterResolution::HalfK,
CommonResolution2::FourEightyP => RouterResolution::FourEightyP,
CommonResolution2::SevenTwentyP => RouterResolution::SevenTwentyP,
CommonResolution2::TenEightyP => RouterResolution::TenEightyP,
}
}

View File

@ -1,4 +0,0 @@
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;

View File

@ -1,425 +0,0 @@
use crate::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio;
use crate::api_adapters::quality::common_quality::CommonQuality;
use crate::api_adapters::resolution::common_resolution::CommonResolution;
use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::commands::deprecated::image_edit::image_edit_models::image_edit_model_to_model_type;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::deprecated::text_to_image::artcraft::handle_text_to_image_artcraft::handle_text_to_image_artcraft;
use crate::commands::deprecated::text_to_image::grok::handle_grok::handle_grok;
use crate::commands::deprecated::text_to_image::midjourney::handle_midjourney::handle_midjourney;
use crate::commands::deprecated::text_to_image::sora::handle_text_to_image_sora::handle_text_to_image_sora;
use crate::commands::deprecated::text_to_image::text_to_image_models::text_to_image_model_to_model_type;
use crate::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::commands::response::shorthand::Response;
use crate::commands::response::success_response_wrapper::SerializeMarker;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::events::sendable_event_trait::SendableEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::state::task_database::TaskDatabase;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
use crate::services::grok::state::grok_image_prompt_queue::GrokImagePromptQueue;
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
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::ux::tauri_command_caller::TauriCommandCaller;
use log::{error, info, warn};
use serde::{Deserialize, Serialize};
use sqlite_database::queries::create_task::{create_task, CreateTaskArgs};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum TextToImageModel {
#[serde(rename = "flux_1_dev")]
Flux1Dev,
#[serde(rename = "flux_1_schnell")]
Flux1Schnell,
#[serde(rename = "flux_pro_11")]
FluxPro11,
#[serde(rename = "flux_pro_11_ultra")]
FluxPro11Ultra,
#[serde(rename = "grok_image")]
GrokImage,
#[serde(rename = "recraft_3")]
Recraft3,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "gpt_image_1p5")]
GptImage1p5,
#[serde(rename = "gpt_image_2")]
GptImage2,
#[serde(rename = "gemini_25_flash")]
Gemini25Flash,
#[serde(rename = "nano_banana")]
NanoBanana,
#[serde(rename = "nano_banana_2")]
NanoBanana2,
#[serde(rename = "nano_banana_pro")]
NanoBananaPro,
#[serde(rename = "seedream_4")]
Seedream4,
#[serde(rename = "seedream_4p5")]
Seedream4p5,
#[serde(rename = "seedream_5_lite")]
Seedream5Lite,
// Generic Midjourney model, version unknown.
#[serde(rename = "midjourney")]
Midjourney,
}
#[derive(Deserialize, Debug)]
pub struct EnqueueTextToImageRequest {
/// The provider to use (defaults to Artcraft/Storyteller).
/// Not all (provider, model) combinations are valid.
pub provider: Option<GenerationProvider>,
/// The model to use.
pub model: Option<TextToImageModel>,
/// Text prompt for the image generation. Required.
pub prompt: Option<String>,
/// New field for aspect ratio.
/// Not all models support each of these aspect ratios, but we can choose or interpolate sensibly.
pub common_aspect_ratio: Option<CommonAspectRatio>,
/// New field for resolution.
/// Not all models support each of these resolutions, but we can choose or interpolate sensibly.
pub common_resolution: Option<CommonResolution>,
/// Quality.
/// Tends to be used by OpenAI models.
pub quality: Option<CommonQuality>,
/// Aspect ratio.
#[deprecated(note="use common_aspect_ratio")]
pub aspect_ratio: Option<TextToImageSize>,
/// Aspect ratio.
#[deprecated(note="use common_resolution")]
pub image_resolution: Option<TextToImageResolution>,
/// The number of images to generate.
pub number_images: Option<u32>,
/// OPTIONAL.
/// Reference images (without semantics)
/// The purpose varies on a model-by-model basis, but they
/// are not semantically treated as "style reference",
/// "character/object reference", etc.
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// OPTIONAL.
/// Name of the frontend caller.
/// We'll use this to selectively trigger events.
pub frontend_caller: Option<TauriCommandCaller>,
/// OPTIONAL.
/// A frontend-defined identifier that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_id: Option<String>,
/// OPTIONAL.
/// A frontend-defined payload that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_payload: Option<String>,
}
// TODO(bt,2025-07-14): Support other aspect ratios / resolutions -
// Flux Dev has: 4:3, 16:9, 3:4, 9:16, and custom.
// Flux Schnell has: 4:3, 16:9, 3:4, 9:16, and custom.
// Flux Pro has: 4:3, 16:9, 3:4, 9:16, and custom.
// Flux Pro Ulra has: 4:3, 16:9, 3:4, 9:16, and custom.
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum TextToImageSize {
Auto,
Square,
Wide,
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 {
}
impl SerializeMarker for EnqueueTextToImageSuccessResponse {}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueTextToImageErrorType {
/// Caller didn't specify a model
ModelNotSpecified,
/// No model available for image generation
NoProviderAvailable,
/// Generic server error
ServerError,
/// No Fal API key available
NeedsFalApiKey,
/// Fal had an API error
FalError,
/// Needs to be logged into Artcraft
NeedsStorytellerCredentials,
}
#[tauri::command]
pub async fn enqueue_text_to_image_command(
request: EnqueueTextToImageRequest,
app: AppHandle,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
mj_creds_manager: State<'_, MidjourneyCredentialManager>,
grok_creds_manager: State<'_, GrokCredentialManager>,
grok_image_prompt_queue: State<'_, GrokImagePromptQueue>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
sora_creds_manager: State<'_, SoraCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
) -> Response<EnqueueTextToImageSuccessResponse, EnqueueTextToImageErrorType, ()> {
info!("enqueue_text_to_image called");
info!("request: {:?}", request);
let result = handle_request(
request,
&app,
&app_data_root,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&mj_creds_manager,
&grok_creds_manager,
&grok_image_prompt_queue,
&storyteller_creds_manager,
&app_env_configs,
&sora_creds_manager,
&sora_task_queue,
).await;
match result {
Err(err) => {
error!("error: {:?}", err);
notify_frontend_of_errors(&app, &err).await;
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueTextToImageErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
match err {
GenerateError::BadInput(BadInputReason::NoModelSpecified) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueTextToImageErrorType::ModelNotSpecified;
error_message = "No model specified for image generation";
}
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueTextToImageErrorType::NoProviderAvailable;
error_message = "No configured provider available for image generation";
}
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey) => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueTextToImageErrorType::NeedsFalApiKey;
error_message = "You need to set a FAL api key";
},
_ => {}, // Fall-through
}
Err(CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
})
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
action: event.to_frontend_event_action(),
service: event.to_frontend_event_service(),
model: event.model,
};
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
CreditsBalanceChangedEvent{}.send_infallible(&app);
Ok(EnqueueTextToImageSuccessResponse {}.into())
}
}
}
pub async fn handle_request(
request: EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
mj_creds_manager: &MidjourneyCredentialManager,
grok_creds_manager: &GrokCredentialManager,
grok_image_prompt_queue: &GrokImagePromptQueue,
storyteller_creds_manager: &StorytellerCredentialManager,
app_env_configs: &AppEnvConfigs,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let result = dispatch_request(
&request,
&app,
&app_data_root,
&provider_priority_store,
&storyteller_creds_manager,
&app_env_configs,
&mj_creds_manager,
&grok_creds_manager,
&grok_image_prompt_queue,
&sora_creds_manager,
&sora_task_queue,
).await;
let success_event = match result {
Err(err) => return Err(err),
Ok(event) => event,
};
let result = success_event
.insert_into_task_database_with_frontend_payload(
task_database,
request.frontend_caller,
request.frontend_subscriber_id.as_deref(),
request.frontend_subscriber_payload.as_deref(),
)
.await;
if let Err(err) = result {
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
let num_images = request.number_images.map(|n| n as u16).unwrap_or(1);
let is_image_to_image = request.image_media_tokens
.as_ref()
.map(|tokens| !tokens.is_empty())
.unwrap_or(false);
let usage_type = if is_image_to_image {
ArtcraftUsageType::ImageToResult
} else {
ArtcraftUsageType::TextToResult
};
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, usage_type, ArtcraftUsagePage::ImagePage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
pub async fn dispatch_request(
request: &EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
provider_priority_store: &ProviderPriorityStore,
storyteller_creds_manager: &StorytellerCredentialManager,
app_env_configs: &AppEnvConfigs,
mj_creds_manager: &MidjourneyCredentialManager,
grok_creds_manager: &GrokCredentialManager,
grok_image_prompt_queue: &GrokImagePromptQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let model = match request.model {
Some(model) => model,
None => {
return Err(GenerateError::no_model_specified())
}
};
let provider = match (model, request.provider) {
(TextToImageModel::GrokImage, _) => GenerationProvider::Grok,
(TextToImageModel::Midjourney, _) => GenerationProvider::Midjourney,
_ => request.provider.unwrap_or(GenerationProvider::Artcraft),
};
info!("generate image with {:?} via provider {:?}", &model, &provider);
match provider {
GenerationProvider::Artcraft => {
handle_text_to_image_artcraft(
model,
request,
app,
app_data_root,
app_env_configs,
storyteller_creds_manager,
).await
}
GenerationProvider::Grok => {
handle_grok(
app,
request,
app_env_configs,
grok_creds_manager,
grok_image_prompt_queue,
).await
}
GenerationProvider::Midjourney => {
handle_midjourney(
app,
request,
app_env_configs,
mj_creds_manager,
).await
}
GenerationProvider::Sora => {
handle_text_to_image_sora(
request,
app,
sora_creds_manager,
sora_task_queue,
).await
}
_ => {
Err(GenerateError::BadProviderForModel {
provider,
model: text_to_image_model_to_model_type(model),
})
}
}
}

View File

@ -1,95 +0,0 @@
use crate::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image::aspect_ratio_to_grok_image;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::events::generation_events::common::GenerationModel;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
use crate::services::grok::state::grok_image_prompt_queue::{GrokImagePromptQueue, PromptItem};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use grok_consumer_client::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio;
use uuid_utils::uuid::generate_random_uuid;
use tauri::AppHandle;
pub async fn handle_grok(
app: &AppHandle,
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
creds_manager: &GrokCredentialManager,
grok_image_prompt_queue: &GrokImagePromptQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
//// TODO: We can request population of the user info if absent or expired.
//let websocket = get_websocket(
// app,
// creds_manager,
// websocket_manager,
//).await?;
//
//let _result = prompt_websocket_image(PromptWebsocketImageArgs {
// websocket_wrapped: &websocket,
// prompt: request.prompt.as_deref().unwrap_or_else(||""),
//}).await?;
//
//let request_id = listen_for_websocket_request_id(ListenForWebsocketRequestIdArgs {
// websocket: &websocket,
// timeout: Duration::from_millis(10_000),
//}).await?;
//
//let request_id = match request_id.request_id {
// Some(request_id) => request_id.to_string(),
// None => {
// warn!("No request id from Grok image generation (websocket)");
// return Err(GenerateError::ProviderFailure(ProviderFailureReason::GrokJobEnqueueFailed));
// }
//};
//
//info!("Successfully enqueued MidJourney. Job token: {}", request_id);
let job_id = generate_random_uuid();
let prompt = request.prompt
.as_deref()
.map(|prompt| prompt.trim().to_string())
.unwrap_or_else(|| "".to_string());
let aspect_ratio = get_aspect_ratio(request);
grok_image_prompt_queue.enqueue(PromptItem {
task_id: job_id.clone(),
prompt,
aspect_ratio: aspect_ratio.unwrap_or(ClientMessageAspectRatio::Square),
})?;
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Grok,
model: Some(GenerationModel::GrokImage),
provider_job_id: Some(job_id),
task_type: TaskType::ImageGeneration,
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}
fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<ClientMessageAspectRatio> {
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
// Handle modern aspect ratio
let aspect = aspect_ratio_to_grok_image(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(ClientMessageAspectRatio::Square),
TextToImageSize::Square => Some(ClientMessageAspectRatio::Square),
TextToImageSize::Wide => Some(ClientMessageAspectRatio::WideThreeByTwo),
TextToImageSize::Tall => Some(ClientMessageAspectRatio::TallTwoByThree),
}
}
None
}

View File

@ -1 +0,0 @@
pub (super) mod handle_grok;

View File

@ -1,143 +0,0 @@
use crate::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::functional_events::canvas_background_removal_complete_event::CanvasBackgroundRemovalCompleteEvent;
use crate::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::events::warning_events::flash_user_input_error_event::FlashUserInputErrorEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::provider_priority::ProviderPriorityStore;
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
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 uuid_utils::uuid::generate_random_uuid;
use log::{error, info};
use midjourney_client::client::midjourney_hostname::MidjourneyHostname;
use midjourney_client::endpoints::submit_job::{submit_job, SubmitJobRequest};
use midjourney_client::error::midjourney_api_error::MidjourneyApiError;
use midjourney_client::recipes::channel_id::ChannelId;
use midjourney_client::recipes::text_to_image::{text_to_image, TextToImageError, TextToImageRequest};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use std::time::Duration;
use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub async fn handle_midjourney(
app: &AppHandle,
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
mj_creds_manager: &MidjourneyCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match mj_creds_manager.maybe_copy_cookie_store() {
Ok(Some(creds)) => creds,
Ok(None) => {
error!("Midjourney credentials not found.");
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Midjourney, &app);
return Err(GenerateError::needs_midjourney_credentials());
}
Err(err) => {
error!("Error reading Midjourney credentials: {:?}", err);
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Midjourney, &app);
return Err(GenerateError::needs_midjourney_credentials());
},
};
// TODO: We can request population of the user info if absent or expired.
let user_info = match mj_creds_manager.maybe_copy_user_info() {
Ok(Some(user_info)) => user_info,
Ok(None) => {
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserInfo));
}
Err(err) => {
error!("Error reading Midjourney user info: {:?}", err);
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserInfo));
},
};
let channel_id = match user_info.user_id {
Some(user_id) => ChannelId::UserId(user_id),
None => {
error!("Midjourney user info does not contain a user ID.");
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserId));
}
};
info!("Calling midjourney ...");
let cookie_header = creds.to_cookie_string();
let prompt = request.prompt
.as_deref()
.unwrap_or("");
let result = text_to_image(TextToImageRequest {
prompt,
channel_id: &channel_id,
hostname: MidjourneyHostname::Standard,
cookie_header,
}).await;
let result = match result {
Ok(result) => result,
Err(err) => {
error!("Failed to use MidJourney: {:?}", err);
return Err(GenerateError::from(err));
}
};
let job_id = match result.maybe_job_id {
Some(job_id) => job_id,
None => {
error!("Failed to enqueue MidJourney: No job ID returned.");
return handle_midjourney_errors(app, result.maybe_errors);
}
};
info!("Successfully enqueued MidJourney. Job token: {}", job_id);
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Midjourney,
model: Some(GenerationModel::Midjourney),
provider_job_id: Some(job_id),
task_type: TaskType::ImageGeneration,
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}
fn handle_midjourney_errors(
app: &AppHandle,
maybe_errors: Option<Vec<TextToImageError>>
) -> Result<TaskEnqueueSuccess, GenerateError> {
if let Some(errors) = maybe_errors {
if !errors.is_empty() {
let messages: Vec<String> = errors.iter()
.map(|e| format!("{:?}", e))
.collect();
let combined_message = messages.join("; ");
let event = FlashUserInputErrorEvent {
message: format!("Midjourney Error: {}", combined_message),
};
if let Err(err) = event.send(&app) {
error!("Failed to send FlashUserInputErrorEvent: {:?}", err); // Fail open
}
}
}
Err(GenerateError::ProviderFailure(ProviderFailureReason::MidjourneyJobEnqueueFailed))
}

View File

@ -1 +0,0 @@
pub (super) mod handle_midjourney;

View File

@ -1,7 +0,0 @@
pub (super) mod text_to_image_models;
pub mod enqueue_text_to_image_command;
mod artcraft;
mod grok;
mod midjourney;
mod sora;

View File

@ -1,138 +0,0 @@
use crate::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image::aspect_ratio_to_grok_image;
use crate::api_adapters::aspect_ratio::convert::aspect_ratio_to_sora_native_gpt_image_1::aspect_ratio_to_sora_native_gpt_image_1;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use errors::AnyhowResult;
use grok_consumer_client::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio;
use log::{error, info};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use std::time::Duration;
use tauri::AppHandle;
const SORA_SIMPLE_IMAGE_GEN_TIMEOUT : Duration = Duration::from_millis(1000 * 30); // 30 seconds
pub async fn handle_gpt_image_1_sora_text_to_image(
request: &EnqueueTextToImageRequest,
app: &AppHandle,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let mut creds = match sora_creds_manager.get_credentials() {
Ok(Some(creds)) => creds,
Ok(None) => {
error!("Sora credentials not found.");
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Sora, &app);
return Err(GenerateError::needs_sora_credentials());
}
Err(err) => {
error!("Error reading Sora credentials: {:?}", err);
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Sora, &app);
return Err(GenerateError::needs_sora_credentials());
}
};
let credential_updated = maybe_upgrade_or_renew_session(&mut creds)
.await
.map_err(|err| {
error!("Failed to upgrade or renew session: {:?}", err);
err
})?;
if credential_updated {
info!("Storing updated credentials");
sora_creds_manager.set_credentials(&creds)?;
}
let prompt = request.prompt.as_deref().unwrap_or("");
let aspect_ratio = get_aspect_ratio(request)
.unwrap_or(ImageSize::Square);
let num_images = match request.number_images {
None => NumImages::One,
Some(1) => NumImages::One,
Some(2) => NumImages::Two,
Some(3) => NumImages::Two, // NB: There is no option for three images!
Some(4) => NumImages::Four,
_ => NumImages::One,
};
let result =
simple_image_gen_with_session_auto_renew(SimpleImageGenAutoRenewRequest {
prompt: prompt.to_string(),
num_images,
image_size: aspect_ratio,
credentials: &creds,
request_timeout: Some(SORA_SIMPLE_IMAGE_GEN_TIMEOUT),
}).await;
let (response, maybe_new_creds) =
match result {
Ok((response, maybe_new_creds)) => (response, maybe_new_creds),
Err(err) => {
let event = GenerationEnqueueFailureEvent {
action: GenerationAction::GenerateImage,
service: GenerationServiceProvider::Sora,
model: None,
reason: None,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(GenerateError::from(err));
}
};
if let Some(new_creds) = maybe_new_creds {
info!("Storing updated credentials.");
sora_creds_manager.set_credentials(&new_creds)?;
}
info!("New Sora Task ID: {:?} ", response.task_id);
sora_task_queue.insert(&response.task_id)?;
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Sora,
task_type: TaskType::ImageGeneration,
model: Some(GenerationModel::GptImage1),
provider_job_id: Some(response.task_id.to_string()),
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}
fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<ImageSize> {
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
// Handle modern aspect ratio
let aspect = aspect_ratio_to_sora_native_gpt_image_1(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(ImageSize::Square),
TextToImageSize::Square => Some(ImageSize::Square),
TextToImageSize::Wide => Some(ImageSize::Wide),
TextToImageSize::Tall => Some(ImageSize::Tall),
}
}
None
}

View File

@ -1,21 +0,0 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::deprecated::text_to_image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
use crate::commands::deprecated::text_to_image::sora::handle_gpt_image_1_sora_text_to_image::handle_gpt_image_1_sora_text_to_image;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use tauri::AppHandle;
pub async fn handle_text_to_image_sora(
request: &EnqueueTextToImageRequest,
app: &AppHandle,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
handle_gpt_image_1_sora_text_to_image(
request,
app,
sora_creds_manager,
sora_task_queue,
).await
}

View File

@ -1,2 +0,0 @@
pub (super) mod handle_text_to_image_sora;
mod handle_gpt_image_1_sora_text_to_image;

View File

@ -1,24 +0,0 @@
use crate::commands::deprecated::text_to_image::enqueue_text_to_image_command::TextToImageModel;
use enums::common::generation::common_model_type::CommonModelType;
pub fn text_to_image_model_to_model_type(model: TextToImageModel) -> CommonModelType {
match model {
TextToImageModel::Flux1Dev => CommonModelType::Flux1Dev,
TextToImageModel::Flux1Schnell => CommonModelType::Flux1Schnell,
TextToImageModel::FluxPro11 => CommonModelType::FluxPro11,
TextToImageModel::FluxPro11Ultra => CommonModelType::FluxPro11Ultra,
TextToImageModel::GrokImage => CommonModelType::GrokImage,
TextToImageModel::Recraft3 => CommonModelType::Recraft3,
TextToImageModel::GptImage1 => CommonModelType::GptImage1,
TextToImageModel::GptImage1p5 => CommonModelType::GptImage1p5,
TextToImageModel::GptImage2 => CommonModelType::GptImage2,
TextToImageModel::Gemini25Flash => CommonModelType::NanoBanana,
TextToImageModel::NanoBanana => CommonModelType::NanoBanana,
TextToImageModel::NanoBanana2 => CommonModelType::NanoBanana2,
TextToImageModel::NanoBananaPro => CommonModelType::NanoBananaPro,
TextToImageModel::Seedream4 => CommonModelType::Seedream4,
TextToImageModel::Seedream4p5 => CommonModelType::Seedream4p5,
TextToImageModel::Seedream5Lite => CommonModelType::Seedream5Lite,
TextToImageModel::Midjourney => CommonModelType::Midjourney,
}
}

View File

@ -1,4 +0,0 @@
pub mod maybe_notify_frontend_of_billing_errors;
pub mod maybe_notify_frontend_of_credential_errors;
pub mod maybe_notify_frontend_of_grok_errors;
pub mod notify_frontend_of_errors;

View File

@ -1,14 +0,0 @@
use crate::commands::enqueue::common::maybe_notify_frontend_of_billing_errors::maybe_notify_frontend_of_billing_errors;
use crate::commands::enqueue::common::maybe_notify_frontend_of_credential_errors::maybe_notify_frontend_of_credential_errors;
use crate::commands::enqueue::common::maybe_notify_frontend_of_grok_errors::maybe_notify_frontend_of_grok_errors;
use crate::commands::enqueue::generate_error::GenerateError;
use tauri::AppHandle;
pub async fn notify_frontend_of_errors(
app: &AppHandle,
error: &GenerateError,
) {
maybe_notify_frontend_of_billing_errors(app, error).await;
maybe_notify_frontend_of_credential_errors(app, error).await;
maybe_notify_frontend_of_grok_errors(app, error).await;
}

View File

@ -1,26 +0,0 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::generation_events::common::GenerationAction;
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use log::error;
use tauri::AppHandle;
pub(super) fn get_storyteller_creds_or_error(
app: &AppHandle,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<StorytellerCredentialSet, GenerateError> {
match storyteller_creds_manager.get_credentials()? {
Some(creds) => Ok(creds),
None => {
error!("No Artcraft credentials are set. Can't perform action.");
let event =
GenerationEnqueueFailureEvent::no_artcraft_credentials(GenerationAction::GenerateGaussian);
event.send_infallible(app);
Err(GenerateError::needs_storyteller_credentials())
}
}
}

View File

@ -1,32 +0,0 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::image_to_gaussian::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::commands::enqueue::image_to_gaussian::artcraft::handle_artcraft_splat_via_router::handle_artcraft_splat_via_router;
use crate::commands::enqueue::image_to_gaussian::enqueue_image_to_gaussian_command::{EnqueueImageToGaussianRequest, GaussianModel};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::generation_events::common::GenerationModel;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use router::api::router_splat_model::RouterSplatModel;
use tauri::AppHandle;
pub async fn handle_gaussian_artcraft(
request: &EnqueueImageToGaussianRequest,
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = get_storyteller_creds_or_error(app, storyteller_creds_manager)?;
match request.model {
None => Err(GenerateError::no_model_specified()),
Some(GaussianModel::Marble0p1Mini) => {
handle_artcraft_splat_via_router(request, app_env_configs, &creds, RouterSplatModel::Marble0p1Mini, GenerationModel::WorldlabsMarble0p1Mini).await
}
Some(
GaussianModel::Marble0p1Plus |
GaussianModel::WorldLabsMarble // Legacy variant — route to Plus
) => {
handle_artcraft_splat_via_router(request, app_env_configs, &creds, RouterSplatModel::Marble0p1Plus, GenerationModel::WorldlabsMarble0p1Plus).await
}
}
}

View File

@ -1,65 +0,0 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::image_to_gaussian::enqueue_image_to_gaussian_command::EnqueueImageToGaussianRequest;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::generation_events::common::GenerationModel;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use router::api::router_splat_model::RouterSplatModel;
use router::api::image_list_ref::ImageListRef;
use router::api::router_provider::RouterProvider;
use router::client::router_artcraft_client::RouterArtcraftClient;
use router::client::router_client::RouterClient;
use router::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use log::{error, info};
pub(super) async fn handle_artcraft_splat_via_router(
request: &EnqueueImageToGaussianRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
model: RouterSplatModel,
generation_model: GenerationModel,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let client = RouterClient::Artcraft(RouterArtcraftClient::new(
app_env_configs.storyteller_host.clone(),
creds.clone(),
));
let router_request = GenerateSplatRequest {
model,
provider: RouterProvider::Artcraft,
prompt: request.prompt.clone(),
reference_images: request.image_media_tokens.clone().map(ImageListRef::MediaFileTokens),
idempotency_token: None,
};
let plan = router_request.build()?;
info!("Splat Generation Plan: {:?}", plan);
let response = match plan.generate_splat(&client).await {
Ok(resp) => {
info!("Successfully enqueued splat generation.");
resp
}
Err(err) => {
error!("Failed to enqueue splat generation: {:?}", 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),
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}

View File

@ -1,3 +0,0 @@
mod get_storyteller_creds_or_error;
mod handle_artcraft_splat_via_router;
pub(super) mod handle_artcraft_gaussian;

View File

@ -1,262 +0,0 @@
use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::commands::enqueue::image_to_gaussian::artcraft::handle_artcraft_gaussian::handle_gaussian_artcraft;
use crate::commands::enqueue::image_to_gaussian::worldlabs::handle_worldlabs_marble::handle_worldlabs_marble;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::commands::response::shorthand::Response;
use crate::commands::response::success_response_wrapper::SerializeMarker;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::events::sendable_event_trait::SendableEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::state::task_database::TaskDatabase;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
use crate::services::grok::state::grok_image_prompt_queue::GrokImagePromptQueue;
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
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 crate::services::worldlabs::state::worldlabs_credential_manager::WorldlabsCredentialManager;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
use log::{error, info, warn};
use serde::{Deserialize, Serialize};
use sqlite_database::queries::create_task::{create_task, CreateTaskArgs};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum GaussianModel {
#[serde(rename = "world_labs_marble")]
WorldLabsMarble,
#[serde(rename = "marble_0p1_mini")]
Marble0p1Mini,
#[serde(rename = "marble_0p1_plus")]
Marble0p1Plus,
}
#[derive(Deserialize)]
pub struct EnqueueImageToGaussianRequest {
/// REQUIRED.
/// The model to use.
pub model: Option<GaussianModel>,
/// OPTIONAL.
/// The provider to use (defaults to Artcraft/Storyteller).
/// Not all (provider, model) combinations are valid.
pub provider: Option<GenerationProvider>,
/// OPTIONAL.
/// Text prompt for the gaussian generation. Required.
pub prompt: Option<String>,
/// OPTIONAL.
/// Images for the gaussian generation
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// OPTIONAL.
/// Name of the frontend caller.
/// We'll use this to selectively trigger events.
pub frontend_caller: Option<TauriCommandCaller>,
/// OPTIONAL.
/// A frontend-defined identifier that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_id: Option<String>,
/// OPTIONAL.
/// A frontend-defined payload that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_payload: Option<String>,
}
#[derive(Serialize)]
pub struct EnqueueImageToGaussianSuccessResponse {
}
impl SerializeMarker for EnqueueImageToGaussianSuccessResponse {}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueImageToGaussianErrorType {
/// Caller didn't specify a model
ModelNotSpecified,
/// No model available for image generation
NoProviderAvailable,
/// Generic server error
ServerError,
/// Needs to be logged into Artcraft
NeedsStorytellerCredentials,
}
#[tauri::command]
pub async fn enqueue_image_to_gaussian_command(
request: EnqueueImageToGaussianRequest,
app: AppHandle,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
worldlabs_creds_manager: State<'_, WorldlabsCredentialManager>,
) -> Response<EnqueueImageToGaussianSuccessResponse, EnqueueImageToGaussianErrorType, ()> {
info!("enqueue_image_to_gaussian called");
let result = handle_request(
request,
&app,
&app_data_root,
&artcraft_usage_tracker,
&task_database,
&storyteller_creds_manager,
&worldlabs_creds_manager,
&app_env_configs,
).await;
match result {
Err(err) => {
error!("error: {:?}", err);
notify_frontend_of_errors(&app, &err).await;
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueImageToGaussianErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
match err {
GenerateError::BadInput(BadInputReason::NoModelSpecified) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageToGaussianErrorType::ModelNotSpecified;
error_message = "No model specified for image generation";
}
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueImageToGaussianErrorType::NoProviderAvailable;
error_message = "No configured provider available for image generation";
}
_ => {}, // Fall-through
}
Err(CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
})
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
action: event.to_frontend_event_action(),
service: event.to_frontend_event_service(),
model: event.model,
};
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
CreditsBalanceChangedEvent{}.send_infallible(&app);
Ok(EnqueueImageToGaussianSuccessResponse {}.into())
}
}
}
pub async fn handle_request(
request: EnqueueImageToGaussianRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
worldlabs_creds_manager: &WorldlabsCredentialManager,
app_env_configs: &AppEnvConfigs,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let result = dispatch_request(
&request,
&app,
&app_data_root,
&storyteller_creds_manager,
&worldlabs_creds_manager,
&app_env_configs,
).await;
let success_event = match result {
Err(err) => return Err(err),
Ok(event) => event,
};
let result = success_event
.insert_into_task_database_with_frontend_payload(
task_database,
request.frontend_caller,
request.frontend_subscriber_id.as_deref(),
request.frontend_subscriber_payload.as_deref(),
)
.await;
if let Err(err) = result {
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_object_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::OtherPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
pub async fn dispatch_request(
request: &EnqueueImageToGaussianRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
worldlabs_creds_manager: &WorldlabsCredentialManager,
app_env_configs: &AppEnvConfigs,
) -> Result<TaskEnqueueSuccess, GenerateError> {
if request.model.is_none() {
return Err(GenerateError::BadInput(BadInputReason::NoModelSpecified));
}
let provider = request.provider.unwrap_or(GenerationProvider::Artcraft);
match provider {
GenerationProvider::WorldLabs => {
handle_worldlabs_marble(
app,
app_data_root,
app_env_configs,
request,
worldlabs_creds_manager,
).await
}
GenerationProvider::Artcraft => {
handle_gaussian_artcraft(
request,
app,
app_env_configs,
storyteller_creds_manager,
).await
}
_ => {
Err(GenerateError::NoProviderAvailable)
}
}
}

View File

@ -1,3 +0,0 @@
mod artcraft;
pub mod enqueue_image_to_gaussian_command;
mod worldlabs;

View File

@ -1,239 +0,0 @@
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::commands::enqueue::image_to_gaussian::enqueue_image_to_gaussian_command::EnqueueImageToGaussianRequest;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::events::generation_events::common::GenerationModel;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::subdirectory::trait_data_subdir::DataSubdir;
use crate::utils::get_url_file_extension::get_url_file_extension;
use crate::utils::simple_http_download::simple_http_download;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use crate::services::worldlabs::state::worldlabs_credential_manager::WorldlabsCredentialManager;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use uuid_utils::uuid::generate_random_uuid;
use log::{error, info};
use std::path::PathBuf;
use artcraft_client::endpoints::media_files::get_media_file::get_media_file;
use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
use worldlabs_consumer_client::credentials::world_labs_cookies::WorldLabsCookies;
use worldlabs_consumer_client::recipes::upload_image_and_create_world_with_retry::{upload_image_and_create_world_with_retry, FileBytesOrPath, UploadImageAndCreateWorldWithRetryArgs};
pub(super) const MAX_IMAGES: usize = 10;
pub async fn handle_worldlabs_marble(
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
request: &EnqueueImageToGaussianRequest,
worldlabs_creds_manager: &WorldlabsCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let world_labs_cookies = match worldlabs_creds_manager.maybe_copy_typed_cookies()? {
Some(cookies) => cookies,
None => {
error!("No WorldLabs cookies!");
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::WorldLabs, &app);
return Err(GenerateError::needs_worldlabs_credentials());
}
};
let world_labs_bearer = match worldlabs_creds_manager.maybe_copy_bearer_token()? {
Some(bearer) => bearer,
None => {
error!("No WorldLabs bearer token!");
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::WorldLabs, &app);
return Err(GenerateError::needs_worldlabs_credentials());
}
};
let world_labs_refresh = match worldlabs_creds_manager.maybe_copy_refresh_token()? {
Some(bearer) => bearer,
None => {
error!("No WorldLabs refresh token!");
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::WorldLabs, &app);
return Err(GenerateError::needs_worldlabs_credentials());
}
};
let maybe_prompt = request.prompt
.as_deref()
.map(|prompt| prompt.trim().to_string());
info!("Downloading image file...");
let file_path = download_file(
app_data_root,
app_env_configs,
request,
).await?;
info!("Enqueueing WorldLabs request...");
let response = upload_image_and_create_world_with_retry(UploadImageAndCreateWorldWithRetryArgs {
cookies: &world_labs_cookies,
bearer_token: &world_labs_bearer,
refresh_token: &world_labs_refresh,
individual_request_timeout: None,
file: FileBytesOrPath::Path(file_path),
}).await?;
let run_id = response.run_id;
let world_id = response.world_id;
info!("Run ID: {:?}", run_id);
info!("World ID: {:?}", world_id);
if let Some(new_access) = response.maybe_new_access_tokens {
info!("New access tokens were generated; saving.");
worldlabs_creds_manager.replace_bearer_and_refresh_token(
new_access.bearer_token,
new_access.refresh_token
)?;
worldlabs_creds_manager.persist_to_disk()?;
}
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::WorldLabs,
model: Some(GenerationModel::WorldlabsMarble),
provider_job_id: Some(world_id.0),
task_type: TaskType::ImageGeneration,
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}
async fn download_file(
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
request: &EnqueueImageToGaussianRequest,
) -> Result<PathBuf, GenerateError> {
let maybe_media_token = request.image_media_tokens
.as_ref()
.and_then(|tokens| tokens.get(0));
let media_token = match maybe_media_token {
Some(token) => token,
None => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages {
min: 1,
max: MAX_IMAGES as u32,
provided: 0,
}));
}
};
info!("Calling get media file API: {:?}", app_env_configs.storyteller_host);
// TODO(bt,2025-12-18): Add a cache.
info!("Using media token: {:?}", media_token);
let response = get_media_file(
&app_env_configs.storyteller_host,
media_token
).await?;
let media_file_url = &response.media_file.media_links.cdn_url;
let extension_with_dot = get_url_file_extension(media_file_url)
.map(|ext| format!(".{}", ext))
.unwrap_or_else(|| ".png".to_string());
let filename = format!("{}{}", response.media_file.token.as_str(), extension_with_dot);
let filename = app_data_root.downloads_dir().path().join(&filename);
simple_http_download(&media_file_url, &filename).await?;
Ok(filename)
}
//async fn download_files(
// app_data_root: &AppDataRoot,
// app_env_configs: &AppEnvConfigs,
// request: &EnqueueImageToGaussianRequest,
//) -> Result<Vec<PathBuf>, GenerateError> {
//
// let maybe_media_token = request.image_media_tokens
// .as_ref()
// .and_then(|tokens| tokens.get(0));
//
//
// let media_tokens = match request.image_media_tokens.as_ref() {
// Some(media_tokens) => media_tokens.to_vec(),
// None => {
// return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages {
// min: 1,
// max: MAX_IMAGES as u32,
// provided: 0,
// }));
// }
// };
//
// 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,
// }));
// }
//
// info!("Calling get media file API: {:?}", app_env_configs.storyteller_host);
//
// let mut local_files_to_upload = Vec::with_capacity(10);
//
// // TODO(bt,2025-07-07): This is inefficient. Cache and parallelize this.
// // TODO(bt,2025-12-18): Add a cache.
// for media_token in media_tokens.iter() {
// info!("Using media token: {:?}", media_token);
//
// let response = get_media_file(
// &app_env_configs.storyteller_host,
// media_token
// ).await?;
//
// let media_file_url = &response.media_file.media_links.cdn_url;
// let extension_with_dot = get_url_file_extension(media_file_url)
// .map(|ext| format!(".{}", ext))
// .unwrap_or_else(|| ".png".to_string());
//
// let filename = format!("{}{}", response.media_file.token.as_str(), extension_with_dot);
// let filename = app_data_root.downloads_dir().path().join(&filename);
//
// simple_http_download(&media_file_url, &filename).await?;
//
// local_files_to_upload.push(filename);
// }
//
// Ok(local_files_to_upload)
//}
// TODO:
// fn handle_midjourney_errors(
// app: &AppHandle,
// maybe_errors: Option<Vec<TextToImageError>>
// ) -> Result<TaskEnqueueSuccess, GenerateError> {
// if let Some(errors) = maybe_errors {
// if !errors.is_empty() {
// let messages: Vec<String> = errors.iter()
// .map(|e| format!("{:?}", e))
// .collect();
//
// let combined_message = messages.join("; ");
//
// let event = FlashUserInputErrorEvent {
// message: format!("Midjourney Error: {}", combined_message),
// };
//
// if let Err(err) = event.send(&app) {
// error!("Failed to send FlashUserInputErrorEvent: {:?}", err); // Fail open
// }
// }
// }
//
// Err(GenerateError::ProviderFailure(ProviderFailureReason::MidjourneyJobEnqueueFailed))
// }

View File

@ -1 +0,0 @@
pub (super) mod handle_worldlabs_marble;

View File

@ -1,159 +0,0 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::image_to_object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_client::api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
use artcraft_client::api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use uuid_utils::uuid::generate_random_uuid;
use log::{error, info};
use artcraft_client::endpoints::generate::object::generate_hunyuan_3d_2_0_image_to_3d::generate_hunyuan3d_2_0_image_to_3d;
use artcraft_client::endpoints::generate::object::generate_hunyuan_3d_2_1_image_to_3d::generate_hunyuan3d_2_1_image_to_3d;
use tauri::AppHandle;
use artcraft_client::api_defs::generate::object::multi_function::hunyuan3d_v3_multi_function_object_gen::Hunyuan3dV3MultiFunctionObjectGenRequest;
use artcraft_client::endpoints::generate::object::multi_function::hunyuan3d_v3_multi_function_object_gen::hunyuan3d_v3_multi_function_object_gen;
pub async fn handle_artcraft_object(
request: &EnqueueImageTo3dObjectRequest,
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
error!("No Artcraft credentials are set. Can't perform action.");
let event =
GenerationEnqueueFailureEvent::no_artcraft_credentials(GenerationAction::ImageTo3d);
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(GenerateError::needs_storyteller_credentials());
},
};
info!("Calling Artcraft image to 3d ...");
let uuid_idempotency_token = generate_random_uuid();
let mut used_model = None;
let job_token = match request.model {
None => {
return Err(GenerateError::no_model_specified());
}
Some(
EnqueueImageTo3dObjectModel::Hunyuan3d2 |
EnqueueImageTo3dObjectModel::Hunyuan3d2_0
) => {
info!("enqueue Artcraft Hunyuan 3D 2.0");
used_model = Some(GenerationModel::Hunyuan3d2_0);
let request = GenerateHunyuan20ImageTo3dRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token.clone(),
};
let result = generate_hunyuan3d_2_0_image_to_3d(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Hunyuan 3D 2.0");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Hunyuan 3D 2.0: {:?}", err);
return Err(GenerateError::from(err));
}
}
}
Some(EnqueueImageTo3dObjectModel::Hunyuan3d2_1) => {
info!("enqueue Artcraft Hunyuan 3D 2.1");
used_model = Some(GenerationModel::Hunyuan3d2_1);
let request = GenerateHunyuan21ImageTo3dRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token.clone(),
};
let result = generate_hunyuan3d_2_1_image_to_3d(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Hunyuan 3D 2.1");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Hunyuan 3D 2.1: {:?}", err);
return Err(GenerateError::from(err));
}
}
}
Some(EnqueueImageTo3dObjectModel::Hunyuan3d3) => {
info!("enqueue Artcraft Hunyuan 3D 3");
used_model = Some(GenerationModel::Hunyuan3d3);
let request = Hunyuan3dV3MultiFunctionObjectGenRequest {
uuid_idempotency_token,
image_media_token: request.image_media_token.clone(),
prompt: None,
back_image_media_token: None,
left_image_media_token: None,
right_image_media_token: None,
face_count: None,
generate_type: None,
polygon_type: None,
enable_pbr: None,
};
let result = hunyuan3d_v3_multi_function_object_gen(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Hunyuan 3D 3");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Hunyuan 3D 3: {:?}", err);
return Err(GenerateError::from(err));
}
}
}
};
info!("Successfully enqueued 3D generation");
//let event = GenerationEnqueueSuccessEvent {
// action: GenerationAction::ImageTo3d,
// service: GenerationServiceProvider::Artcraft,
// model: None,
//};
//
//if let Err(err) = event.send(app) {
// error!("Failed to emit event: {:?}", err); // Fail open.
//}
Ok(TaskEnqueueSuccess {
task_type: TaskType::ObjectGeneration,
model: used_model,
provider: GenerationProvider::Artcraft,
provider_job_id: Some(job_token.to_string()),
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}

View File

@ -1 +0,0 @@
pub (super) mod handle_artcraft_object;

View File

@ -1,217 +0,0 @@
use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::commands::enqueue::image_to_object::artcraft::handle_artcraft_object::handle_artcraft_object;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::commands::response::shorthand::Response;
use crate::commands::response::success_response_wrapper::SerializeMarker;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
use crate::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::state::task_database::TaskDatabase;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
use log::{error, info, warn};
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
#[derive(Deserialize)]
pub struct EnqueueImageTo3dObjectRequest {
/// Image media file; the image to remove the background from.
/// TODO: In the future we may support base64 images, URLs, or file paths here.
pub image_media_token: Option<MediaFileToken>,
/// The model to use.
pub model: Option<EnqueueImageTo3dObjectModel>,
/// OPTIONAL.
/// Name of the frontend caller.
/// We'll use this to selectively trigger events.
pub frontend_caller: Option<TauriCommandCaller>,
/// OPTIONAL.
/// A frontend-defined identifier that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_id: Option<String>,
/// OPTIONAL.
/// A frontend-defined payload that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_payload: Option<String>,
}
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueImageTo3dObjectModel {
#[deprecated(note="Use `hunyuan_3d_2_0` instead")]
#[serde(rename = "hunyuan_3d_2")]
Hunyuan3d2,
#[serde(rename = "hunyuan_3d_2_0")]
Hunyuan3d2_0,
#[serde(rename = "hunyuan_3d_2_1")]
Hunyuan3d2_1,
#[serde(rename = "hunyuan_3d_3")]
Hunyuan3d3,
}
#[derive(Serialize)]
pub struct EnqueueImageTo3dObjectSuccessResponse {
}
impl SerializeMarker for EnqueueImageTo3dObjectSuccessResponse {}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueImageTo3dObjectErrorType {
/// Caller didn't specify a model
ModelNotSpecified,
/// No model available for object generation
NoProviderAvailable,
/// Generic server error
ServerError,
/// No Fal API key available
NeedsFalApiKey,
/// Fal had an API error
FalError,
/// Needs to be logged into Artcraft
NeedsStorytellerCredentials,
}
#[tauri::command]
pub async fn enqueue_image_to_3d_object_command(
app: AppHandle,
request: EnqueueImageTo3dObjectRequest,
app_env_configs: State<'_, AppEnvConfigs>,
app_data_root: State<'_, AppDataRoot>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
) -> Response<EnqueueImageTo3dObjectSuccessResponse, EnqueueImageTo3dObjectErrorType, ()> {
info!("enqueue_image_to_3d_object_command called");
let result = handle_request(
request,
&app,
&app_env_configs,
&app_data_root,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
).await;
match result {
Err(err) => {
error!("error: {:?}", err);
notify_frontend_of_errors(&app, &err).await;
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueImageTo3dObjectErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
match err {
GenerateError::BadInput(BadInputReason::NoModelSpecified) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageTo3dObjectErrorType::ModelNotSpecified;
error_message = "No model specified for object generation";
}
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueImageTo3dObjectErrorType::NoProviderAvailable;
error_message = "No configured provider available for object generation";
}
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey) => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageTo3dObjectErrorType::NeedsFalApiKey;
error_message = "You need to set a FAL api key";
},
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials) => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageTo3dObjectErrorType::NeedsStorytellerCredentials;
error_message = "You need to be logged into Artcraft.";
}
_ => {}, // Fall-through
}
Err(CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
})
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
action: event.to_frontend_event_action(),
service: event.to_frontend_event_service(),
model: event.model,
};
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
CreditsBalanceChangedEvent{}.send_infallible(&app);
Ok(EnqueueImageTo3dObjectSuccessResponse {}.into())
}
}
}
pub async fn handle_request(
request: EnqueueImageTo3dObjectRequest,
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let result = handle_artcraft_object(
&request,
&app,
&app_env_configs,
&app_data_root,
&storyteller_creds_manager,
).await;
let success_event = match result {
Err(err) => return Err(err),
Ok(event) => event,
};
let result = success_event
.insert_into_task_database_with_frontend_payload(
task_database,
request.frontend_caller.clone(),
request.frontend_subscriber_id.as_deref(),
request.frontend_subscriber_payload.as_deref(),
)
.await;
if let Err(err) = result {
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_object_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::ObjectPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}

View File

@ -1,2 +0,0 @@
pub mod enqueue_image_to_3d_object_command;
mod artcraft;

View File

@ -1,6 +0,0 @@
pub mod common;
pub mod generate_error;
pub mod image_bg_removal;
pub mod image_to_gaussian;
pub mod image_to_object;
pub mod task_enqueue_success;

View File

@ -1,6 +1,6 @@
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::commands::enqueue::generate_error::{CredentialProblemReason, GenerateError};
use crate::commands::generate::generate_error::{CredentialProblemReason, GenerateError};
use crate::credentials::credential::Credential;
use crate::state::data_dir::app_data_root::AppDataRoot;

View File

@ -1,4 +1,4 @@
use crate::commands::enqueue::generate_error::{BillingIssueReason, BillingProvider, GenerateError, ProviderFailureReason};
use crate::commands::generate::generate_error::{BillingIssueReason, BillingProvider, GenerateError, ProviderFailureReason};
use crate::events::functional_events::show_provider_billing_modal_event::ShowProviderBillingModalEvent;
use enums::common::generation_provider::GenerationProvider;
use log::warn;

View File

@ -1,6 +1,6 @@
use tauri::AppHandle;
use crate::commands::enqueue::generate_error::{CredentialProblemReason, GenerateError};
use crate::commands::generate::generate_error::{CredentialProblemReason, GenerateError};
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::functional_events::show_credential_error_modal_event::ShowCredentialErrorModalEvent;

View File

@ -1,4 +1,4 @@
use crate::commands::enqueue::generate_error::{GenerateError, ProviderFailureReason};
use crate::commands::generate::generate_error::{GenerateError, ProviderFailureReason};
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;

View File

@ -1,3 +1,7 @@
pub mod maybe_notify_frontend_of_billing_errors;
pub mod maybe_notify_frontend_of_credential_errors;
pub mod maybe_notify_frontend_of_grok_errors;
pub mod notify_frontend_of_errors;
pub mod generation_credential;
pub mod router_image_request_to_artcraft_prompt;
pub mod router_video_request_to_artcraft_prompt;

View File

@ -0,0 +1,14 @@
use crate::commands::generate::common::maybe_notify_frontend_of_billing_errors::maybe_notify_frontend_of_billing_errors;
use crate::commands::generate::common::maybe_notify_frontend_of_credential_errors::maybe_notify_frontend_of_credential_errors;
use crate::commands::generate::common::maybe_notify_frontend_of_grok_errors::maybe_notify_frontend_of_grok_errors;
use crate::commands::generate::generate_error::GenerateError;
use tauri::AppHandle;
pub async fn notify_frontend_of_errors(
app: &AppHandle,
error: &GenerateError,
) {
maybe_notify_frontend_of_billing_errors(app, error).await;
maybe_notify_frontend_of_credential_errors(app, error).await;
maybe_notify_frontend_of_grok_errors(app, error).await;
}

View File

@ -1,6 +1,6 @@
use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_audio::handle_credential_router::handle_credential_router;
use crate::commands::generate::generate_audio::request::{
TauriGenerateAudioErrorType, TauriGenerateAudioRequest, TauriGenerateAudioResponse,

View File

@ -12,8 +12,8 @@ use router::api::image_list_ref::ImageListRef;
use crate::api_adapters::models::audio::tauri_audio_model_to_generation_model::tauri_audio_model_to_generation_model;
use crate::api_adapters::models::audio::tauri_audio_model_to_router_model::tauri_audio_model_to_router_model;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::common::generation_credential::{credential_not_usable, resolve_generation_credential, storyteller_creds_from_credential};
use crate::commands::generate::generate_audio::request::TauriGenerateAudioRequest;
use crate::credentials::artcraft_api_host::maybe_artcraft_api_host_for_service;

View File

@ -1,6 +1,6 @@
use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::generate::generate_error::{GenerateError, MissingCredentialsReason};
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_image::providers::router::handle_credential_router::handle_credential_router;
use crate::commands::generate::generate_image::tauri_generate_image_request::{
TauriGenerateImageErrorType, TauriGenerateImageRequest, TauriGenerateImageResponse,

View File

@ -1,7 +1,7 @@
use log::info;
use crate::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::{GenerateError, MissingCredentialsReason};
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_image::providers::artcraft::handle_artcraft_via_legacy::handle_artcraft_via_legacy;
use crate::commands::generate::generate_image::providers::artcraft::handle_artcraft_via_omni_endpoint::handle_artcraft_via_omni_endpoint;
use crate::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;

View File

@ -1,7 +1,7 @@
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_image::providers::artcraft::legacy::artcraft_flux_2_lora_angles::handle_flux_2_lora_angles;
use crate::commands::generate::generate_image::providers::artcraft::legacy::artcraft_flux_dev_juggernaut_inpaint::handle_flux_dev_juggernaut_inpaint;
use crate::commands::generate::generate_image::providers::artcraft::legacy::artcraft_flux_pro_kontext_edit::handle_flux_pro_kontext_edit;

View File

@ -7,8 +7,8 @@ use log::{error, info};
use tokens::tokens::media_files::MediaFileToken;
use uuid_utils::uuid::generate_random_uuid;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::api_adapters::models::image::tauri_image_model_to_enums_model::tauri_image_model_to_enums_model;
use crate::api_adapters::models::image::tauri_image_model_to_generation_model::tauri_image_model_to_generation_model;
use crate::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;

View File

@ -12,8 +12,8 @@ use router::generate::generate_image::image_generation_draft_or_request::ImageGe
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
use crate::events::generation_events::common::GenerationModel;

View File

@ -11,8 +11,8 @@ use uuid_utils::uuid::generate_random_uuid;
use tokens::tokens::media_files::MediaFileToken;
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::{BadInputReason, GenerateError};
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
use crate::events::generation_events::common::GenerationModel;

View File

@ -10,8 +10,8 @@ use enums::tauri::tasks::task_type::TaskType;
use tokens::tokens::media_files::MediaFileToken;
use uuid_utils::uuid::generate_random_uuid;
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::{BadInputReason, GenerateError};
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
use crate::events::generation_events::common::GenerationModel;

View File

@ -12,8 +12,8 @@ use router::generate::generate_image::image_generation_draft_or_request::ImageGe
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
use crate::events::generation_events::common::GenerationModel;

View File

@ -1,92 +0,0 @@
use crate::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image::aspect_ratio_to_grok_image;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::events::generation_events::common::GenerationModel;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
use crate::services::grok::state::grok_image_prompt_queue::{GrokImagePromptQueue, PromptItem};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use grok_consumer_client::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio;
use uuid_utils::uuid::generate_random_uuid;
use tauri::AppHandle;
pub async fn handle_grok(
app: &AppHandle,
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
creds_manager: &GrokCredentialManager,
grok_image_prompt_queue: &GrokImagePromptQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
//// TODO: We can request population of the user info if absent or expired.
//let websocket = get_websocket(
// app,
// creds_manager,
// websocket_manager,
//).await?;
//
//let _result = prompt_websocket_image(PromptWebsocketImageArgs {
// websocket_wrapped: &websocket,
// prompt: request.prompt.as_deref().unwrap_or_else(||""),
//}).await?;
//
//let request_id = listen_for_websocket_request_id(ListenForWebsocketRequestIdArgs {
// websocket: &websocket,
// timeout: Duration::from_millis(10_000),
//}).await?;
//
//let request_id = match request_id.request_id {
// Some(request_id) => request_id.to_string(),
// None => {
// warn!("No request id from Grok image generation (websocket)");
// return Err(GenerateError::ProviderFailure(ProviderFailureReason::GrokJobEnqueueFailed));
// }
//};
//
//info!("Successfully enqueued MidJourney. Job token: {}", request_id);
let job_id = generate_random_uuid();
let prompt = request.prompt
.as_deref()
.map(|prompt| prompt.trim().to_string())
.unwrap_or_else(|| "".to_string());
let aspect_ratio = get_aspect_ratio(request);
grok_image_prompt_queue.enqueue(PromptItem {
task_id: job_id.clone(),
prompt,
aspect_ratio: aspect_ratio.unwrap_or(ClientMessageAspectRatio::Square),
})?;
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Grok,
model: Some(GenerationModel::GrokImage),
provider_job_id: Some(job_id),
task_type: TaskType::ImageGeneration,
})
}
fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<ClientMessageAspectRatio> {
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
// Handle modern aspect ratio
let aspect = aspect_ratio_to_grok_image(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(ClientMessageAspectRatio::Square),
TextToImageSize::Square => Some(ClientMessageAspectRatio::Square),
TextToImageSize::Wide => Some(ClientMessageAspectRatio::WideThreeByTwo),
TextToImageSize::Tall => Some(ClientMessageAspectRatio::TallTwoByThree),
}
}
None
}

View File

@ -1 +0,0 @@
pub (super) mod handle_grok;

View File

@ -1,140 +0,0 @@
use crate::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::functional_events::canvas_background_removal_complete_event::CanvasBackgroundRemovalCompleteEvent;
use crate::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::events::warning_events::flash_user_input_error_event::FlashUserInputErrorEvent;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::provider_priority::ProviderPriorityStore;
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
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 uuid_utils::uuid::generate_random_uuid;
use log::{error, info};
use midjourney_client::client::midjourney_hostname::MidjourneyHostname;
use midjourney_client::endpoints::submit_job::{submit_job, SubmitJobRequest};
use midjourney_client::error::midjourney_api_error::MidjourneyApiError;
use midjourney_client::recipes::channel_id::ChannelId;
use midjourney_client::recipes::text_to_image::{text_to_image, TextToImageError, TextToImageRequest};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use std::time::Duration;
use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
pub async fn handle_midjourney(
app: &AppHandle,
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
mj_creds_manager: &MidjourneyCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match mj_creds_manager.maybe_copy_cookie_store() {
Ok(Some(creds)) => creds,
Ok(None) => {
error!("Midjourney credentials not found.");
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Midjourney, &app);
return Err(GenerateError::needs_midjourney_credentials());
}
Err(err) => {
error!("Error reading Midjourney credentials: {:?}", err);
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Midjourney, &app);
return Err(GenerateError::needs_midjourney_credentials());
},
};
// TODO: We can request population of the user info if absent or expired.
let user_info = match mj_creds_manager.maybe_copy_user_info() {
Ok(Some(user_info)) => user_info,
Ok(None) => {
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserInfo));
}
Err(err) => {
error!("Error reading Midjourney user info: {:?}", err);
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserInfo));
},
};
let channel_id = match user_info.user_id {
Some(user_id) => ChannelId::UserId(user_id),
None => {
error!("Midjourney user info does not contain a user ID.");
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserId));
}
};
info!("Calling midjourney ...");
let cookie_header = creds.to_cookie_string();
let prompt = request.prompt
.as_deref()
.unwrap_or("");
let result = text_to_image(TextToImageRequest {
prompt,
channel_id: &channel_id,
hostname: MidjourneyHostname::Standard,
cookie_header,
}).await;
let result = match result {
Ok(result) => result,
Err(err) => {
error!("Failed to use MidJourney: {:?}", err);
return Err(GenerateError::from(err));
}
};
let job_id = match result.maybe_job_id {
Some(job_id) => job_id,
None => {
error!("Failed to enqueue MidJourney: No job ID returned.");
return handle_midjourney_errors(app, result.maybe_errors);
}
};
info!("Successfully enqueued MidJourney. Job token: {}", job_id);
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Midjourney,
model: Some(GenerationModel::Midjourney),
provider_job_id: Some(job_id),
task_type: TaskType::ImageGeneration,
})
}
fn handle_midjourney_errors(
app: &AppHandle,
maybe_errors: Option<Vec<TextToImageError>>
) -> Result<TaskEnqueueSuccess, GenerateError> {
if let Some(errors) = maybe_errors {
if !errors.is_empty() {
let messages: Vec<String> = errors.iter()
.map(|e| format!("{:?}", e))
.collect();
let combined_message = messages.join("; ");
let event = FlashUserInputErrorEvent {
message: format!("Midjourney Error: {}", combined_message),
};
if let Err(err) = event.send(&app) {
error!("Failed to send FlashUserInputErrorEvent: {:?}", err); // Fail open
}
}
}
Err(GenerateError::ProviderFailure(ProviderFailureReason::MidjourneyJobEnqueueFailed))
}

View File

@ -1 +0,0 @@
pub (super) mod handle_midjourney;

View File

@ -17,8 +17,8 @@ use tokens::tokens::prompts::PromptToken;
use crate::api_adapters::models::image::tauri_image_model_to_generation_model::tauri_image_model_to_generation_model;
use crate::api_adapters::models::image::tauri_image_model_to_router_model::tauri_image_model_to_router_model;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::common::router_image_request_to_artcraft_prompt::router_image_request_to_artcraft_prompt;
use crate::commands::generate::generate_image::providers::router::utils::convert_enums_to_router::{convert_aspect_ratio, convert_quality, convert_resolution};
use crate::commands::generate::generate_image::providers::router::utils::map_media_files_to_urls::map_media_file_tokens_to_cdn_urls;

View File

@ -13,8 +13,8 @@ use tokens::tokens::media_files::MediaFileToken;
use crate::api_adapters::models::image::tauri_image_model_to_generation_model::tauri_image_model_to_generation_model;
use crate::api_adapters::models::image::tauri_image_model_to_router_model::tauri_image_model_to_router_model;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::common::generation_credential::{credential_not_usable, resolve_generation_credential, storyteller_creds_from_credential};
use crate::commands::generate::generate_image::providers::router::handle_api_providers::handle_api_key_provider;
use crate::commands::generate::generate_image::providers::router::utils::convert_enums_to_router::{convert_aspect_ratio, convert_quality, convert_resolution};

View File

@ -1,8 +1,8 @@
use enums::common::generation_provider::GenerationProvider;
use log::info;
use crate::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::{GenerateError, MissingCredentialsReason};
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_image::providers::router::handle_api_providers::handle_api_key_provider;
use crate::commands::generate::generate_image::providers::router::handle_web_login_providers::handle_web_login_provider;
use crate::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;

View File

@ -1,7 +1,7 @@
use enums::common::generation_provider::GenerationProvider;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::providers::credentials::payload::web_login::WebLoginData;

View File

@ -3,7 +3,7 @@ use artcraft_client::utils::api_host::ApiHost;
use log::info;
use tokens::tokens::media_files::MediaFileToken;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::generate::generate_error::GenerateError;
/// Resolve a list of media file tokens to their CDN URLs.
///

View File

@ -1,135 +0,0 @@
use crate::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image::aspect_ratio_to_grok_image;
use crate::api_adapters::aspect_ratio::convert::aspect_ratio_to_sora_native_gpt_image_1::aspect_ratio_to_sora_native_gpt_image_1;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use errors::AnyhowResult;
use grok_consumer_client::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio;
use log::{error, info};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use std::time::Duration;
use tauri::AppHandle;
const SORA_SIMPLE_IMAGE_GEN_TIMEOUT : Duration = Duration::from_millis(1000 * 30); // 30 seconds
pub async fn handle_gpt_image_1_sora_text_to_image(
request: &EnqueueTextToImageRequest,
app: &AppHandle,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let mut creds = match sora_creds_manager.get_credentials() {
Ok(Some(creds)) => creds,
Ok(None) => {
error!("Sora credentials not found.");
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Sora, &app);
return Err(GenerateError::needs_sora_credentials());
}
Err(err) => {
error!("Error reading Sora credentials: {:?}", err);
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Sora, &app);
return Err(GenerateError::needs_sora_credentials());
}
};
let credential_updated = maybe_upgrade_or_renew_session(&mut creds)
.await
.map_err(|err| {
error!("Failed to upgrade or renew session: {:?}", err);
err
})?;
if credential_updated {
info!("Storing updated credentials");
sora_creds_manager.set_credentials(&creds)?;
}
let prompt = request.prompt.as_deref().unwrap_or("");
let aspect_ratio = get_aspect_ratio(request)
.unwrap_or(ImageSize::Square);
let num_images = match request.number_images {
None => NumImages::One,
Some(1) => NumImages::One,
Some(2) => NumImages::Two,
Some(3) => NumImages::Two, // NB: There is no option for three images!
Some(4) => NumImages::Four,
_ => NumImages::One,
};
let result =
simple_image_gen_with_session_auto_renew(SimpleImageGenAutoRenewRequest {
prompt: prompt.to_string(),
num_images,
image_size: aspect_ratio,
credentials: &creds,
request_timeout: Some(SORA_SIMPLE_IMAGE_GEN_TIMEOUT),
}).await;
let (response, maybe_new_creds) =
match result {
Ok((response, maybe_new_creds)) => (response, maybe_new_creds),
Err(err) => {
let event = GenerationEnqueueFailureEvent {
action: GenerationAction::GenerateImage,
service: GenerationServiceProvider::Sora,
model: None,
reason: None,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(GenerateError::from(err));
}
};
if let Some(new_creds) = maybe_new_creds {
info!("Storing updated credentials.");
sora_creds_manager.set_credentials(&new_creds)?;
}
info!("New Sora Task ID: {:?} ", response.task_id);
sora_task_queue.insert(&response.task_id)?;
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Sora,
task_type: TaskType::ImageGeneration,
model: Some(GenerationModel::GptImage1),
provider_job_id: Some(response.task_id.to_string()),
})
}
fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<ImageSize> {
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
// Handle modern aspect ratio
let aspect = aspect_ratio_to_sora_native_gpt_image_1(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(ImageSize::Square),
TextToImageSize::Square => Some(ImageSize::Square),
TextToImageSize::Wide => Some(ImageSize::Wide),
TextToImageSize::Tall => Some(ImageSize::Tall),
}
}
None
}

View File

@ -1,21 +0,0 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::deprecated::text_to_image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
use crate::commands::generate::generate_image::sora::handle_gpt_image_1_sora_text_to_image::handle_gpt_image_1_sora_text_to_image;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use tauri::AppHandle;
pub async fn handle_text_to_image_sora(
request: &EnqueueTextToImageRequest,
app: &AppHandle,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
handle_gpt_image_1_sora_text_to_image(
request,
app,
sora_creds_manager,
sora_task_queue,
).await
}

View File

@ -1,2 +0,0 @@
pub (super) mod handle_text_to_image_sora;
mod handle_gpt_image_1_sora_text_to_image;

View File

@ -4,7 +4,7 @@ use artcraft_client::credentials::storyteller_credential_set::StorytellerCredent
use artcraft_client::utils::api_host::ApiHost;
use tokens::tokens::media_files::MediaFileToken;
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::generate::generate_error::{BadInputReason, GenerateError};
use crate::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::utils::upload_bytes_to_media_file::upload_image_bytes_as_media_file::upload_image_bytes_as_media_file;
use crate::utils::upload_bytes_to_media_file::upload_mask_image_bytes_as_media_file::upload_mask_image_bytes_as_media_file;

View File

@ -1,6 +1,6 @@
use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_mesh::handle_credential_router::handle_credential_router;
use crate::commands::generate::generate_mesh::request::{
TauriGenerateMeshErrorType, TauriGenerateMeshRequest, TauriGenerateMeshResponse,

View File

@ -13,8 +13,8 @@ use router::api::mesh_ref::MeshRef;
use crate::api_adapters::models::mesh::tauri_mesh_model_to_generation_model::tauri_mesh_model_to_generation_model;
use crate::api_adapters::models::mesh::tauri_mesh_model_to_router_model::tauri_mesh_model_to_router_model;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::common::generation_credential::{credential_not_usable, resolve_generation_credential, storyteller_creds_from_credential};
use crate::commands::generate::generate_mesh::request::TauriGenerateMeshRequest;
use crate::credentials::artcraft_api_host::maybe_artcraft_api_host_for_service;

View File

@ -71,9 +71,9 @@ pub struct TauriGenerateMeshRequest {
/// model ids (`CommonMeshModel` serde strings).
#[derive(Clone, Copy, Debug, Deserialize)]
pub enum TauriMeshModel {
#[serde(rename = "hunyuan_3d_2p0")]
#[serde(rename = "hunyuan_3d_2p0", alias = "hunyuan_3d_2_0", alias = "hunyuan_3d_2")]
Hunyuan3d2p0,
#[serde(rename = "hunyuan_3d_2p1")]
#[serde(rename = "hunyuan_3d_2p1", alias = "hunyuan_3d_2_1")]
Hunyuan3d2p1,
#[serde(rename = "hunyuan_3d_3")]
Hunyuan3d3,

View File

@ -1,6 +1,6 @@
use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_splat::handle_credential_router::handle_credential_router;
use crate::commands::generate::generate_splat::request::{
TauriGenerateSplatErrorType, TauriGenerateSplatRequest, TauriGenerateSplatResponse,

View File

@ -12,8 +12,8 @@ use router::api::video_ref::VideoRef;
use crate::api_adapters::models::splat::tauri_splat_model_to_generation_model::tauri_splat_model_to_generation_model;
use crate::api_adapters::models::splat::tauri_splat_model_to_router_model::tauri_splat_model_to_router_model;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::common::generation_credential::{credential_not_usable, resolve_generation_credential, storyteller_creds_from_credential};
use crate::commands::generate::generate_splat::request::TauriGenerateSplatRequest;
use crate::credentials::artcraft_api_host::maybe_artcraft_api_host_for_service;

View File

@ -1,4 +1,4 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::generate::generate_error::GenerateError;
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::events::generation_events::common::GenerationAction;
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;

View File

@ -1,7 +1,7 @@
use crate::api_adapters::models::video::tauri_video_model_to_generation_model::tauri_video_model_to_generation_model;
use crate::api_adapters::models::video::tauri_video_model_to_router_model::tauri_video_model_to_router_model;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
use crate::commands::generate::generate_video::artcraft::handle_artcraft_video_via_router::handle_artcraft_video_via_router;
use crate::commands::generate::generate_video::request::TauriGenerateVideoRequest;

View File

@ -1,6 +1,6 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::generate_video::request::TauriGenerateVideoRequest;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::generation_events::common::GenerationModel;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_client::utils::api_host::ApiHost;

View File

@ -1,6 +1,6 @@
use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::commands::generate::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_video::handle_credential_router::handle_credential_router;
use crate::commands::generate::generate_video::request::{
TauriGenerateVideoErrorType, TauriGenerateVideoRequest, TauriGenerateVideoResponse,

View File

@ -1,152 +0,0 @@
use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::commands::deprecated::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
use crate::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::commands::generate::generate_video::request::{TauriGenerateVideoRequest, TauriVideoModel, GrokAspectRatio, SoraOrientation};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::events::generation_events::common::GenerationModel;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
use crate::services::grok::util::upload_image_media_file_to_grok::{upload_image_media_file_to_grok, UploadImageMediaFileToGrok};
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::sora::utils::upload_images_to_sora::{upload_images_to_sora, UploadImagesToSoraArgs};
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use grok_consumer_client::credentials::grok_cookies::GrokCookies;
use grok_consumer_client::credentials::grok_full_credentials::GrokFullCredentials;
use grok_consumer_client::datatypes::api::aspect_ratio::AspectRatio;
use grok_consumer_client::datatypes::file_upload_spec::FileUploadSpec;
use grok_consumer_client::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs};
use grok_consumer_client::recipes::upload_image_and_generate_video::{upload_image_and_generate_video, UploadImageAndGenerateVideo};
use grok_consumer_client::recipes::upload_image_and_generate_video_with_retry::{upload_image_and_generate_video_with_retry, UploadImageAndGenerateVideoWithRetry};
use log::{error, info, warn};
use openai_sora_client::recipes::generate_sora2_video::generate_sora2_video_with_session_auto_renew::generate_sora2_video_with_session_auto_renew;
use openai_sora_client::requests::generate_sora2_video::generate_sora2_video::{GenerateSora2VideoArgs, Orientation};
use std::time::Duration;
use tauri::AppHandle;
const GROK_IMAGE_UPLOAD_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds
pub async fn handle_grok_video(
request: &TauriGenerateVideoRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
grok_credential_manager: &GrokCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = get_grok_creds(app, grok_credential_manager).await?;
let image_token = match request.image_media_token.as_ref() {
Some(token) => token,
None => {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages { min: 1, max: 1, provided: 0 }))
}
};
info!("Downloading image media file...");
let local_image_file = download_media_file_to_temp_dir(
&app_env_configs,
&app_data_root,
image_token,
).await?;
let aspect_ratio = match request.grok_aspect_ratio {
None => AspectRatio::WideThreeByTwo,
Some(GrokAspectRatio::Landscape) => AspectRatio::WideThreeByTwo,
Some(GrokAspectRatio::Portrait) => AspectRatio::TallTwoByThree,
Some(GrokAspectRatio::Square) => AspectRatio::Square,
};
info!("Calling Grok Video generate...");
let upload_result = upload_image_and_generate_video_with_retry(UploadImageAndGenerateVideoWithRetry {
credentials: &creds,
file: FileUploadSpec::Path(local_image_file.path()),
prompt: request.prompt.as_deref(),
aspect_ratio: Some(aspect_ratio),
wait_for_generation: false,
individual_request_timeout: Some(GROK_IMAGE_UPLOAD_TIMEOUT),
mode: None,
}).await;
let post_id = match upload_result {
Err(err) => {
error!("Failed to use Grok video: {:?}", err);
return Err(GenerateError::from(err));
}
Ok(result) => {
if let Some(secrets) = result.maybe_new_client_secrets {
info!("New Grok secrets generated! Storing them in the credential manager...");
grok_credential_manager.replace_client_secrets_only(secrets)?;
}
info!("Successfully enqueued Grok Video: post_id = {:?}", result.upload_result.post_id);
result.upload_result.post_id
}
};
// NB: Grok's consumer API doesn't take a version selector; this only affects
// which model is recorded for the task queue / events.
let generation_model = match request.model {
Some(TauriVideoModel::GrokImagineVideo1p5) => GenerationModel::GrokImagineVideo1p5,
_ => GenerationModel::GrokVideo,
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Grok,
model: Some(generation_model),
provider_job_id: Some(post_id.to_string()),
task_type: TaskType::VideoGeneration,
maybe_queue_status_url: None,
maybe_prompt_token: None,
maybe_queue_response_url: None,
})
}
async fn get_grok_creds(app: &AppHandle, grok_credential_manager: &GrokCredentialManager) -> Result<GrokFullCredentials, GenerateError> {
if let Some(creds) = grok_credential_manager.maybe_copy_full_credentials()? {
return Ok(creds);
}
let cookies = match grok_credential_manager.maybe_copy_cookie_store()? {
Some(cookies) => cookies.to_cookie_string(),
None => {
warn!("No Grok Cookie stored. Must login.");
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Grok, &app);
return Err(GenerateError::needs_grok_credentials())
}
};
let cookies = GrokCookies::new(cookies);
info!("Requesting Grok client secrets...");
let upgraded = request_client_secrets(RequestClientSecretsArgs {
cookies: &cookies,
}).await;
match upgraded {
Err(err) => {
error!("Failed to fetch Grok client secrets: {}", err); // NB: Fall-through
}
Ok(secrets) => {
info!("Grok client secrets successfully upgraded...");
let full_creds = GrokFullCredentials::from_cookies_and_client_secrets(cookies, secrets);
grok_credential_manager.replace_full_credentials(full_creds.clone())?;
info!("Persisting grok credentials to disk...");
grok_credential_manager.persist_to_disk()?;
return Ok(full_creds)
}
}
warn!("Grok upgrade failed. Try logging in again...");
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Grok, &app);
Err(GenerateError::needs_grok_credentials())
}

View File

@ -1 +0,0 @@
pub (super) mod handle_grok_video;

View File

@ -2,8 +2,8 @@ use log::info;
use crate::api_adapters::models::video::tauri_video_model_to_generation_model::tauri_video_model_to_generation_model;
use crate::api_adapters::models::video::tauri_video_model_to_router_model::tauri_video_model_to_router_model;
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::common::generation_credential::{credential_not_usable, resolve_generation_credential, storyteller_creds_from_credential};
use crate::commands::generate::generate_video::artcraft::handle_artcraft_video_via_router::handle_artcraft_video_via_router;
use crate::commands::generate::generate_video::request::TauriGenerateVideoRequest;

View File

@ -2,5 +2,4 @@ pub mod generate_video_command;
mod handle_credential_router;
pub mod request;
pub mod artcraft;
pub mod grok;
pub mod sora2;

View File

@ -1,6 +1,6 @@
use crate::commands::enqueue::generate_error::GenerateError;
use crate::commands::generate::generate_error::GenerateError;
use crate::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
use crate::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::events::generation_events::common::GenerationModel;
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;

View File

@ -4,4 +4,6 @@ pub mod generate_mesh;
pub mod generate_splat;
pub mod generate_video;
pub mod common;
pub mod generate_error;
pub mod task_enqueue_success;
pub mod models;

Some files were not shown because too many files have changed in this diff Show More