refactor wip

This commit is contained in:
Brandon Thomas committed 2026-05-07 23:05:51 -04:00
1 parent 302ee8ca68
commit 00288aacf2
18 files changed
+1088 -264

No files matched your search

@@ -0,0 +1,112 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_api_defs::generate::image::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,
})
}
@@ -0,0 +1,46 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_router::api::common_image_model::CommonImageModel;
use 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, CommonImageModel::Flux1Dev, GenerationModel::Flux1Dev).await,
TextToImageModel::Flux1Schnell => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Flux1Schnell, GenerationModel::Flux1Schnell).await,
TextToImageModel::FluxPro11 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::FluxPro11, GenerationModel::FluxPro11).await,
TextToImageModel::FluxPro11Ultra => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::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, CommonImageModel::GptImage1p5, GenerationModel::GptImage1p5).await,
TextToImageModel::GptImage2 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::GptImage2, GenerationModel::GptImage2).await,
TextToImageModel::Gemini25Flash | TextToImageModel::NanoBanana => {
handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanoBanana, GenerationModel::NanoBanana).await
},
TextToImageModel::NanoBanana2 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanoBanana2, GenerationModel::NanoBanana2).await,
TextToImageModel::NanoBananaPro => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanoBananaPro, GenerationModel::NanoBananaPro).await,
TextToImageModel::Seedream4 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream4, GenerationModel::Seedream4).await,
TextToImageModel::Seedream4p5 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream4p5, GenerationModel::Seedream4p5).await,
TextToImageModel::Seedream5Lite => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream5Lite, GenerationModel::Seedream5Lite).await,
}
}
@@ -0,0 +1,159 @@
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio as CommonAspectRatio2;
use crate::core::api_adapters::resolution::common_resolution::CommonResolution as CommonResolution2;
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{
EnqueueTextToImageRequest, TextToImageResolution, TextToImageSize,
};
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterCommonAspectRatio;
use artcraft_router::api::common_image_model::CommonImageModel;
use artcraft_router::api::common_resolution::CommonResolution as RouterCommonResolution;
use artcraft_router::api::common_quality::CommonQuality as RouterCommonQuality;
use artcraft_router::api::image_list_ref::ImageListRef;
use artcraft_router::api::provider::Provider;
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
use artcraft_router::client::router_client::RouterClient;
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use log::{error, info};
pub(super) async fn handle_text_to_image_artcraft_via_router(
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
model: CommonImageModel,
generation_model: GenerationModel,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials)),
};
let client = RouterClient::Artcraft(RouterArtcraftClient::new(
app_env_configs.storyteller_host.clone(),
creds,
));
let image_inputs = request.image_media_tokens.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 = GenerateImageRequest {
model,
provider: Provider::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 plan = router_request.build()?;
info!("Image Generation Plan: {:?}", plan);
let response = match plan.generate_image(&client).await {
Ok(resp) => {
info!("Successfully enqueued.");
resp
}
Err(err) => {
error!("Failed to enqueue: {:?}", err);
return Err(GenerateError::from(err));
}
};
let job_id = response
.get_artcraft_payload()
.map(|p| p.inference_job_token.to_string())
.ok_or(GenerateError::ResponseHadNoJobTokens)?;
Ok(TaskEnqueueSuccess {
task_type: TaskType::ImageGeneration,
model: Some(generation_model),
provider: GenerationProvider::Artcraft,
provider_job_id: Some(job_id),
})
}
fn get_aspect_ratio_t2i(request: &EnqueueTextToImageRequest) -> Option<RouterCommonAspectRatio> {
if let Some(ar) = request.common_aspect_ratio {
return Some(convert_desktop_aspect_ratio(ar));
}
if let Some(ar) = request.aspect_ratio {
return Some(match ar {
TextToImageSize::Auto => RouterCommonAspectRatio::Auto,
TextToImageSize::Square => RouterCommonAspectRatio::Square,
TextToImageSize::Wide => RouterCommonAspectRatio::Wide,
TextToImageSize::Tall => RouterCommonAspectRatio::Tall,
});
}
None
}
fn get_resolution_t2i(request: &EnqueueTextToImageRequest) -> Option<RouterCommonResolution> {
if let Some(res) = request.common_resolution {
return Some(convert_desktop_resolution(res));
}
if let Some(res) = request.image_resolution {
return Some(match res {
TextToImageResolution::OneK => RouterCommonResolution::OneK,
TextToImageResolution::TwoK => RouterCommonResolution::TwoK,
TextToImageResolution::FourK => RouterCommonResolution::FourK,
});
}
None
}
fn get_quality_t2i(request: &EnqueueTextToImageRequest) -> Option<RouterCommonQuality> {
request.quality.map(|quality| quality.to_artcraft_router_type())
}
fn convert_desktop_aspect_ratio(ar: CommonAspectRatio2) -> RouterCommonAspectRatio {
match ar {
CommonAspectRatio2::Auto => RouterCommonAspectRatio::Auto,
CommonAspectRatio2::Square => RouterCommonAspectRatio::Square,
CommonAspectRatio2::WideThreeByTwo => RouterCommonAspectRatio::WideThreeByTwo,
CommonAspectRatio2::WideFourByThree => RouterCommonAspectRatio::WideFourByThree,
CommonAspectRatio2::WideFiveByFour => RouterCommonAspectRatio::WideFiveByFour,
CommonAspectRatio2::WideSixteenByNine => RouterCommonAspectRatio::WideSixteenByNine,
CommonAspectRatio2::WideTwentyOneByNine => RouterCommonAspectRatio::WideTwentyOneByNine,
CommonAspectRatio2::TallTwoByThree => RouterCommonAspectRatio::TallTwoByThree,
CommonAspectRatio2::TallThreeByFour => RouterCommonAspectRatio::TallThreeByFour,
CommonAspectRatio2::TallFourByFive => RouterCommonAspectRatio::TallFourByFive,
CommonAspectRatio2::TallNineBySixteen => RouterCommonAspectRatio::TallNineBySixteen,
CommonAspectRatio2::TallNineByTwentyOne => RouterCommonAspectRatio::TallNineByTwentyOne,
CommonAspectRatio2::Wide => RouterCommonAspectRatio::Wide,
CommonAspectRatio2::Tall => RouterCommonAspectRatio::Tall,
CommonAspectRatio2::Auto2k => RouterCommonAspectRatio::Auto2k,
CommonAspectRatio2::Auto4k => RouterCommonAspectRatio::Auto4k,
CommonAspectRatio2::SquareHd => RouterCommonAspectRatio::SquareHd,
}
}
fn convert_desktop_resolution(res: CommonResolution2) -> RouterCommonResolution {
match res {
CommonResolution2::OneK => RouterCommonResolution::OneK,
CommonResolution2::TwoK => RouterCommonResolution::TwoK,
CommonResolution2::ThreeK => RouterCommonResolution::ThreeK,
CommonResolution2::FourK => RouterCommonResolution::FourK,
CommonResolution2::HalfK => RouterCommonResolution::HalfK,
CommonResolution2::FourEightyP => RouterCommonResolution::FourEightyP,
CommonResolution2::SevenTwentyP => RouterCommonResolution::SevenTwentyP,
CommonResolution2::TenEightyP => RouterCommonResolution::TenEightyP,
}
}
@@ -0,0 +1,4 @@
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;
@@ -7,10 +7,10 @@ use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{
enqueue_text_to_image_command, EnqueueTextToImageRequest, TextToImageModel,
};
use crate::core::commands::generate::generate_image::request::{
use crate::core::commands::generate::generate_image::tauri_generate_image_request::{
TauriGenerateImageErrorType, TauriGenerateImageRequest, TauriGenerateImageResponse,
TauriImageModel,
};
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
@@ -0,0 +1,92 @@
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image::aspect_ratio_to_grok_image;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::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_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
}
@@ -0,0 +1 @@
pub (super) mod handle_grok;
@@ -0,0 +1,140 @@
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::canvas_background_removal_complete_event::CanvasBackgroundRemovalCompleteEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::warning_events::flash_user_input_error_event::FlashUserInputErrorEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::services::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))
}
@@ -0,0 +1 @@
pub (super) mod handle_midjourney;
@@ -1,2 +1,3 @@
pub mod generate_image_command;
pub mod request;
pub mod tauri_generate_image_request;
pub mod tauri_image_model;
@@ -0,0 +1,135 @@
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image::aspect_ratio_to_grok_image;
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_sora_native_gpt_image_1::aspect_ratio_to_sora_native_gpt_image_1;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::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_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
}
@@ -0,0 +1,21 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
use crate::core::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
}
@@ -0,0 +1,2 @@
pub (super) mod handle_text_to_image_sora;
mod handle_gpt_image_1_sora_text_to_image;
@@ -1,106 +1,14 @@
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio;
use artcraft_router::api::common_resolution::CommonResolution;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::generation::common_quality::CommonQuality;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
// ── Model ──
/// Unified image model enum covering text-to-image, image edit, and inpainting.
///
/// 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 TauriImageModel {
// Text-to-image models
#[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,
#[serde(rename = "midjourney")]
Midjourney,
// Image edit models
#[serde(rename = "flux_pro_kontext_max")]
FluxProKontextMax,
#[serde(rename = "qwen_edit_2511_angles")]
QwenEdit2511Angles,
#[serde(rename = "flux_2_lora_angles")]
Flux2LoraAngles,
// Inpainting models
#[serde(rename = "flux_dev_juggernaut")]
FluxDevJuggernaut,
#[serde(rename = "flux_pro_1")]
FluxPro1,
}
impl TauriImageModel {
pub fn to_common_model_type(&self) -> CommonModelType {
match self {
Self::Flux1Dev => CommonModelType::Flux1Dev,
Self::Flux1Schnell => CommonModelType::Flux1Schnell,
Self::FluxPro11 => CommonModelType::FluxPro11,
Self::FluxPro11Ultra => CommonModelType::FluxPro11Ultra,
Self::GrokImage => CommonModelType::GrokImage,
Self::Recraft3 => CommonModelType::Recraft3,
Self::GptImage1 => CommonModelType::GptImage1,
Self::GptImage1p5 => CommonModelType::GptImage1p5,
Self::GptImage2 => CommonModelType::GptImage2,
Self::Gemini25Flash => CommonModelType::NanoBanana,
Self::NanoBanana => CommonModelType::NanoBanana,
Self::NanoBanana2 => CommonModelType::NanoBanana2,
Self::NanoBananaPro => CommonModelType::NanoBananaPro,
Self::Seedream4 => CommonModelType::Seedream4,
Self::Seedream4p5 => CommonModelType::Seedream4p5,
Self::Seedream5Lite => CommonModelType::Seedream5Lite,
Self::Midjourney => CommonModelType::Midjourney,
Self::FluxProKontextMax => CommonModelType::FluxProKontextMax,
Self::QwenEdit2511Angles => CommonModelType::QwenEdit2511Angles,
Self::Flux2LoraAngles => CommonModelType::Flux2LoraAngles,
Self::FluxDevJuggernaut => CommonModelType::FluxDevJuggernaut,
Self::FluxPro1 => CommonModelType::FluxPro1,
}
}
}
// ── Request ──
#[derive(Deserialize, Debug)]
@@ -0,0 +1,92 @@
use enums::common::generation::common_model_type::CommonModelType;
use serde_derive::Deserialize;
/// Unified image model enum covering text-to-image, image edit, and inpainting.
///
/// 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 TauriImageModel {
// Text-to-image models
#[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,
#[serde(rename = "midjourney")]
Midjourney,
// Image edit models
#[serde(rename = "flux_pro_kontext_max")]
FluxProKontextMax,
#[serde(rename = "qwen_edit_2511_angles")]
QwenEdit2511Angles,
#[serde(rename = "flux_2_lora_angles")]
Flux2LoraAngles,
// Inpainting models
#[serde(rename = "flux_dev_juggernaut")]
FluxDevJuggernaut,
#[serde(rename = "flux_pro_1")]
FluxPro1,
}
impl TauriImageModel {
pub fn to_common_model_type(&self) -> CommonModelType {
match self {
Self::Flux1Dev => CommonModelType::Flux1Dev,
Self::Flux1Schnell => CommonModelType::Flux1Schnell,
Self::FluxPro11 => CommonModelType::FluxPro11,
Self::FluxPro11Ultra => CommonModelType::FluxPro11Ultra,
Self::GrokImage => CommonModelType::GrokImage,
Self::Recraft3 => CommonModelType::Recraft3,
Self::GptImage1 => CommonModelType::GptImage1,
Self::GptImage1p5 => CommonModelType::GptImage1p5,
Self::GptImage2 => CommonModelType::GptImage2,
Self::Gemini25Flash => CommonModelType::NanoBanana,
Self::NanoBanana => CommonModelType::NanoBanana,
Self::NanoBanana2 => CommonModelType::NanoBanana2,
Self::NanoBananaPro => CommonModelType::NanoBananaPro,
Self::Seedream4 => CommonModelType::Seedream4,
Self::Seedream4p5 => CommonModelType::Seedream4p5,
Self::Seedream5Lite => CommonModelType::Seedream5Lite,
Self::Midjourney => CommonModelType::Midjourney,
Self::FluxProKontextMax => CommonModelType::FluxProKontextMax,
Self::QwenEdit2511Angles => CommonModelType::QwenEdit2511Angles,
Self::Flux2LoraAngles => CommonModelType::Flux2LoraAngles,
Self::FluxDevJuggernaut => CommonModelType::FluxDevJuggernaut,
Self::FluxPro1 => CommonModelType::FluxPro1,
}
}
}
+1
View File
@@ -13,6 +13,7 @@ export * from "./lib/enqueue/EnqueueImageTo3dObject.js";
export * from "./lib/enqueue/EnqueueImageTo3dWorld.js";
export * from "./lib/enqueue/EnqueueImageToGaussian.js";
export * from "./lib/enqueue/EnqueueImageToVideo.js";
export * from "./lib/enqueue/GenerateVideoCommand.js";
export * from "./lib/enqueue/EnqueueTextToImage.js";
export * from "./lib/generate/GenerateImage.js";
export * from "./lib/generate/GenerateVideo.js";
@@ -0,0 +1,192 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
import { Model } from "@storyteller/model-list";
import { GenerationProvider } from "@storyteller/api-enums";
export enum GenerateVideoErrorType {
/// Caller didn't specify a model
ModelNotSpecified = "model_not_specified",
/// Generic server error
ServerError = "server_error",
/// No Fal API key available
NeedsFalApiKey = "needs_fal_api_key",
/// Fal had an API error
FalError = "fal_error",
}
export interface GenerateVideoRequest {
// Required. The model to use.
model?: Model;
// The provider to use.
provider?: GenerationProvider;
// Optional. Starting frame.
start_frame_image_media_token?: string;
// Optional. Starting frame (legacy field name).
image_media_token?: string;
// Optional. Ending frame.
end_frame_image_media_token?: string;
// Optional. Text prompt to direct the video.
prompt?: string;
// Optional frontend state to return later.
frontend_caller?: string;
// Optional frontend state to return later.
frontend_subscriber_id?: string;
// Optional. Orientation of the video for Sora 2.
sora_orientation?: "portrait" | "landscape";
// Optional. Aspect Ratio for the video for Grok Video.
grok_aspect_ratio?: "portrait" | "landscape" | "square";
// Optional. Whether to generate audio alongside the video (used by some models like Veo2)
generate_audio?: boolean;
// Optional. Common aspect ratio for other models.
// Not all are supported by all models, but Artcraft will compensate.
aspect_ratio?: string; // TODO: Typesafety.
// Optional. Duration in seconds.
duration_seconds?: number;
// Optional. Reference image media tokens (for reference mode).
reference_image_media_tokens?: string[];
// Optional. Reference video media tokens (for video-to-video reference).
reference_video_media_tokens?: string[];
// Optional. Reference audio media tokens (for audio reference).
reference_audio_media_tokens?: string[];
// Optional. Character tokens for character consistency (from @-mentions).
reference_character_tokens?: string[];
}
interface RawGenerateVideoRequest {
model?: string;
provider?: GenerationProvider;
start_frame_image_media_token?: string;
image_media_token?: string;
end_frame_image_media_token?: string;
prompt?: string;
frontend_caller?: string;
frontend_subscriber_id?: string;
sora_orientation?: "portrait" | "landscape";
grok_aspect_ratio?: "portrait" | "landscape" | "square";
generate_audio?: boolean;
aspect_ratio?: string;
duration_seconds?: number;
reference_image_media_tokens?: string[];
reference_video_media_tokens?: string[];
reference_audio_media_tokens?: string[];
reference_character_tokens?: string[];
}
export interface GenerateVideoError extends CommandResult {
error_type: GenerateVideoErrorType;
error_message?: string;
}
export type GenerateVideoPayload = Record<string, never>;
export interface GenerateVideoSuccess extends CommandResult {
payload: GenerateVideoPayload;
}
export type GenerateVideoResult =
| GenerateVideoSuccess
| GenerateVideoError;
export const GenerateVideoCommand = async (
request: GenerateVideoRequest,
): Promise<GenerateVideoResult> => {
const mutableRequest: RawGenerateVideoRequest = {
model: request.model?.tauriId,
start_frame_image_media_token: request.start_frame_image_media_token,
image_media_token: request.image_media_token,
};
if (request.provider) {
mutableRequest.provider = request.provider;
}
if (request.prompt) {
mutableRequest.prompt = request.prompt;
}
if (request.end_frame_image_media_token) {
mutableRequest.end_frame_image_media_token =
request.end_frame_image_media_token;
}
if (request.frontend_caller) {
mutableRequest.frontend_caller = request.frontend_caller;
}
if (request.frontend_subscriber_id) {
mutableRequest.frontend_subscriber_id = request.frontend_subscriber_id;
}
if (request.sora_orientation) {
mutableRequest.sora_orientation = request.sora_orientation;
}
if (request.grok_aspect_ratio) {
mutableRequest.grok_aspect_ratio = request.grok_aspect_ratio;
}
if (typeof request.generate_audio === "boolean") {
mutableRequest.generate_audio = request.generate_audio;
}
if (request.aspect_ratio) {
mutableRequest.aspect_ratio = request.aspect_ratio;
}
if (typeof request.duration_seconds === "number") {
mutableRequest.duration_seconds = request.duration_seconds;
}
if (
request.reference_image_media_tokens &&
request.reference_image_media_tokens.length > 0
) {
mutableRequest.reference_image_media_tokens =
request.reference_image_media_tokens;
}
if (
request.reference_video_media_tokens &&
request.reference_video_media_tokens.length > 0
) {
mutableRequest.reference_video_media_tokens =
request.reference_video_media_tokens;
}
if (
request.reference_audio_media_tokens &&
request.reference_audio_media_tokens.length > 0
) {
mutableRequest.reference_audio_media_tokens =
request.reference_audio_media_tokens;
}
if (
request.reference_character_tokens &&
request.reference_character_tokens.length > 0
) {
mutableRequest.reference_character_tokens = request.reference_character_tokens;
}
const result = await invoke("generate_video_command", {
request: mutableRequest,
});
return result as GenerateVideoResult;
};
+85 -168
View File
File diff suppressed because it is too large. Load diff