artcraft provider wip

This commit is contained in:
Brandon Thomas
2026-04-30 22:36:58 -04:00
parent 7f7356f78f
commit fbb65df591
6 changed files with 131 additions and 321 deletions
@@ -1,112 +0,0 @@
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,129 @@
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
use artcraft_client::endpoints::omni_gen::generate::image::omni_gen_image::omni_gen_image_generate;
use enums::common::generation::common_image_model::CommonImageModel;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use log::{error, info};
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
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;
pub async fn handle_artcraft_via_omni_endpoint(
request: &TauriGenerateImageRequest,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let model = request.model.ok_or(GenerateError::no_model_specified())?;
let omni_model = map_to_common_image_model(model)
.ok_or(GenerateError::NotYetImplemented(
format!("Model {:?} is not supported via the omni endpoint", model),
))?;
let generation_model = map_to_generation_model(model);
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials)),
};
// Build the omni gen request.
let mut image_media_tokens = request.image_media_tokens.clone().unwrap_or_default();
// Prepend canvas image if present.
if let Some(canvas_token) = &request.canvas_image_media_token {
image_media_tokens.insert(0, canvas_token.clone());
}
let image_media_tokens = if image_media_tokens.is_empty() { None } else { Some(image_media_tokens) };
let omni_request = OmniGenImageCostAndGenerateRequest {
idempotency_token: None,
model: Some(omni_model),
prompt: request.prompt.clone(),
image_media_tokens,
resolution: None, // TODO: Convert from request.resolution
aspect_ratio: None, // TODO: Convert from request.aspect_ratio
quality: request.quality,
image_batch_count: request.batch_size.map(|n| n as u16),
adjust_horizontal_angle: request.adjust_horizontal_angle,
adjust_vertical_angle: request.adjust_vertical_angle,
adjust_zoom: request.adjust_zoom,
};
info!("Sending image generation via omni endpoint: model={:?}", omni_model);
let response = omni_gen_image_generate(
&app_env_configs.storyteller_host,
Some(&creds),
omni_request,
).await.map_err(|err| {
error!("Omni image generation failed: {:?}", err);
GenerateError::from(err)
})?;
info!("Omni image generation succeeded: job_token={}", response.inference_job_token.as_str());
Ok(TaskEnqueueSuccess {
task_type: TaskType::ImageGeneration,
model: Some(generation_model),
provider: GenerationProvider::Artcraft,
provider_job_id: Some(response.inference_job_token.to_string()),
})
}
// ── Model mapping helpers ──
fn map_to_common_image_model(model: TauriImageModel) -> Option<CommonImageModel> {
match model {
TauriImageModel::Flux1Dev => Some(CommonImageModel::Flux1Dev),
TauriImageModel::Flux1Schnell => Some(CommonImageModel::Flux1Schnell),
TauriImageModel::FluxPro11 => Some(CommonImageModel::FluxPro11),
TauriImageModel::FluxPro11Ultra => Some(CommonImageModel::FluxPro11Ultra),
TauriImageModel::Gemini25Flash => Some(CommonImageModel::NanoBanana), // NB: Alias for Nano Banana
TauriImageModel::GptImage1 => Some(CommonImageModel::GptImage1),
TauriImageModel::GptImage1p5 => Some(CommonImageModel::GptImage1p5),
TauriImageModel::GptImage2 => Some(CommonImageModel::GptImage2),
TauriImageModel::NanoBanana => Some(CommonImageModel::NanoBanana),
TauriImageModel::NanoBanana2 => Some(CommonImageModel::NanoBanana2),
TauriImageModel::NanoBananaPro => Some(CommonImageModel::NanoBananaPro),
TauriImageModel::Seedream4 => Some(CommonImageModel::Seedream4),
TauriImageModel::Seedream4p5 => Some(CommonImageModel::Seedream4p5),
TauriImageModel::Seedream5Lite => Some(CommonImageModel::Seedream5Lite),
// Models not in CommonImageModel (Grok, Recraft, Midjourney, edit-only, inpaint-only)
_ => None,
}
}
fn map_to_generation_model(model: TauriImageModel) -> GenerationModel {
match model {
TauriImageModel::Flux1Dev => GenerationModel::Flux1Dev,
TauriImageModel::Flux1Schnell => GenerationModel::Flux1Schnell,
TauriImageModel::Flux2LoraAngles => GenerationModel::Flux2LoraAngles,
TauriImageModel::FluxDevJuggernaut => GenerationModel::FluxDevJuggernaut,
TauriImageModel::FluxPro1 => GenerationModel::FluxPro1,
TauriImageModel::FluxPro11 => GenerationModel::FluxPro11,
TauriImageModel::FluxPro11Ultra => GenerationModel::FluxPro11Ultra,
TauriImageModel::FluxProKontextMax => GenerationModel::FluxProKontextMax,
TauriImageModel::Gemini25Flash => GenerationModel::NanoBanana, // NB: Alias for Nano Banana
TauriImageModel::GptImage1 => GenerationModel::GptImage1,
TauriImageModel::GptImage1p5 => GenerationModel::GptImage1p5,
TauriImageModel::GptImage2 => GenerationModel::GptImage2,
TauriImageModel::GrokImage => GenerationModel::GrokImage,
TauriImageModel::NanoBanana => GenerationModel::NanoBanana,
TauriImageModel::NanoBanana2 => GenerationModel::NanoBanana2,
TauriImageModel::NanoBananaPro => GenerationModel::NanoBananaPro,
TauriImageModel::QwenEdit2511Angles => GenerationModel::QwenEdit2511Angles,
TauriImageModel::Seedream4 => GenerationModel::Seedream4,
TauriImageModel::Seedream4p5 => GenerationModel::Seedream4p5,
TauriImageModel::Seedream5Lite => GenerationModel::Seedream5Lite,
// TODO:
TauriImageModel::Midjourney => GenerationModel::Flux1Dev, // Fallback
TauriImageModel::Recraft3 => GenerationModel::Flux1Dev, // Fallback
}
}
@@ -1,46 +0,0 @@
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,
}
}
@@ -1,159 +0,0 @@
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,
}
}
@@ -1,4 +1 @@
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;
pub(super) mod handle_artcraft_via_omni_endpoint;
@@ -1,3 +1,4 @@
mod artcraft;
pub mod generate_image_command;
pub mod providers;
pub mod tauri_generate_image_request;