Compare commits

..
Author SHA1 Message Date
Brandon Thomas 5da97bc531 billing modal component 2025-08-30 20:32:51 -04:00
Brandon Thomas 7082cf2830 billing events (wip) 2025-08-30 20:13:40 -04:00
Brandon Thomas de4a70009d notification hook 2025-08-30 19:01:25 -04:00
Brandon Thomas 77ec1fb43a remove final bespoke errors 2025-08-30 17:56:42 -04:00
Brandon Thomas 12be88789e remove error 2025-08-30 17:20:05 -04:00
Brandon Thomas b7ce347f14 get rid of inpaint error 2025-08-30 17:18:13 -04:00
Brandon Thomas def1d6afda errors for object gen 2025-08-30 16:44:28 -04:00
Brandon Thomas 507e53a5da update errors for video gen 2025-08-30 16:36:15 -04:00
Brandon Thomas f313fa4bd8 remove unused type 2025-08-30 16:23:03 -04:00
Brandon Thomas de055856e2 adjust error messages for text-to-image 2025-08-30 16:06:43 -04:00
Brandon Thomas c6830a892b refactor error type 2025-08-30 11:36:37 -04:00
Brandon Thomas 889feec4be handle error in client lib 2025-08-30 10:05:17 -04:00
Brandon Thomas aea9b32507 note 2025-08-30 09:49:00 -04:00
Brandon Thomas 1cdf5b1118 server payments error codes 2025-08-30 09:47:16 -04:00
Brandon Thomas ddc33ce600 prevent loading gigabytes of images when opening the gallery 2025-08-28 23:41:33 -04:00
BFlatandBrandon Thomas c017fbc25a new text to image prompt box image prompts (#800)
- image prompts linked to models (maximum and if it's allowed or not)
- lots of styling updates
- promptbox doesn't overflow bottom anymore (dynamic height detection)
- selected image prompts can be clicked to be previewed in a modal
<img width="938" height="472" alt="image"
src="https://github.com/user-attachments/assets/e4fc7445-0031-4adf-a6f6-dbcbe1c3ea67"
/>
- @echelon on handleEnqueue - image prompts need to be added

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2025-08-28 23:10:16 -04:00
Brandon Thomas 7557e586ba fix broken transaction 2025-08-28 22:05:45 -04:00
Brandon Thomas 2286d663ac more fal models (#802) 2025-08-28 21:18:26 -04:00
Tanish Manku 4181e24f7f Fix history stack being empty coming from gallery edit button (#799) 2025-08-28 22:59:07 +07:00
BFlat d4911f226b new top menu gifs (#801) 2025-08-28 22:58:13 +07:00
bflatastic 93db726c19 bug fix: inpainting clear button to reset generating state too so it doesn't get stuck 2025-08-28 18:29:41 +07:00
bflatastic dac64fb38c remove unused animated line css 2025-08-28 14:14:51 +07:00
bflatastic 60009f98ac generating animated canvas border, history stack sticky icon top, lock further generation (will need to persist states for page changes) 2025-08-28 14:12:24 +07:00
Tanish Manku 92b68d3f9d Make the inpainting stroke translucent blue (#797) 2025-08-27 15:54:06 -07:00
Tanish Manku c39651415d Fix lossy History Stack (#796) 2025-08-27 14:43:07 -07:00
Brandon Thomas 7dae90f1e2 start work on consolidating internal errors in http server 2025-08-27 17:41:43 -04:00
Brandon Thomas 30634e8be9 fix the model list, wrong tauri ids, new feature predicate (#798) 2025-08-27 17:17:13 -04:00
Brandon Thomas 801886d7a8 adjust enums, disable kontext on text-to-image (for now) (#795) 2025-08-27 04:11:14 -04:00
Brandon Thomas 16bb5d0771 cleanup enum 2025-08-27 03:33:07 -04:00
BFlat 9b1b7f1ad3 styling fixes for history stack (#794)
- clear all tooltip
- padding, gap, ordering fixes
2025-08-27 14:32:03 +07:00
Brandon Thomas 67b66411d7 support flux pro kontext as an "inpainting" model 2025-08-27 03:24:46 -04:00
Brandon Thomas 9fda7657d2 swap order of history stack 2025-08-27 01:49:04 -04:00
Brandon Thomas e3cb9456d0 reorder the tabs 2025-08-27 01:26:29 -04:00
Brandon Thomas 13af7b0664 fix environmental cdn issues with dev (#793) 2025-08-27 01:09:42 -04:00
Brandon Thomas 8250162d8f cleanup model metadata for prompt box (#792) 2025-08-27 00:34:08 -04:00
Brandon Thomas 4852abae72 more typesafe classes for models 2025-08-26 21:03:00 -04:00
Brandon Thomas aa03618d6c video end frame support (#791) 2025-08-26 19:13:15 -04:00
Brandon Thomas 6199f1f5a7 bypass sec-fetch-mode: no-cors for 3d (#787) 2025-08-26 00:19:10 -04:00
Brandon Thomas 27ae474797 more fal models (fal API bindings only) (#786) 2025-08-25 23:20:51 -04:00
Tanish Manku 9d3d3aa98c Merge the delete buttons to history stack (#785) 2025-08-25 16:24:29 -07:00
252 changed files with 7407 additions and 2069 deletions
@@ -1,14 +1,23 @@
use crate::error::fal_error_plus::FalErrorPlus;
const BILLING_ERROR : &str = "{\"detail\": \"User is locked. Reason: Exhausted balance. Top up your balance at fal.ai/dashboard/billing.\"}";
/// Better classify the `fal::FalError` into a more user-friendly `FalErrorPlus`.
pub fn classify_fal_error(err: fal::FalError) -> FalErrorPlus {
// Other("{\"detail\": \"User is locked. Reason: Exhausted balance. Top up your balance at fal.ai/dashboard/billing.\"}")
match err {
fal::FalError::RequestError(_) => FalErrorPlus::FalError(err),
fal::FalError::ImageError(_) => FalErrorPlus::FalError(err),
fal::FalError::SerializeError(_) => FalErrorPlus::FalError(err),
fal::FalError::StreamError(_) => FalErrorPlus::FalError(err),
fal::FalError::Other(ref inner) => {
if inner.contains("Invalid Key Authorization header format") {
if inner == BILLING_ERROR {
FalErrorPlus::FalBillingError(inner.to_string())
} else if inner.contains("billing") {
FalErrorPlus::FalBillingError(inner.to_string())
} else if inner.contains("balance") {
FalErrorPlus::FalBillingError(inner.to_string())
} else if inner.contains("Invalid Key Authorization header format") {
FalErrorPlus::FalApiKeyError(inner.to_string())
} else if inner.contains("No user found for Key ID and Secret") {
FalErrorPlus::FalApiKeyError(inner.to_string())
@@ -9,6 +9,8 @@ pub enum FalErrorPlus {
FalError(fal::FalError),
/// The fal API key is invalid.
FalApiKeyError(String),
/// The fal account has a billing issue
FalBillingError(String),
/// Another error we didn't handle.
AnyhowError(anyhow::Error),
/// URL parse errors.
@@ -24,6 +26,7 @@ impl Display for FalErrorPlus {
match self {
Self::FalError(err) => write!(f, "FalErrorPlus::FalError: {:?}", err),
Self::FalApiKeyError(reason) => write!(f, "FalErrorPlus::FalApiKeyError: {}", reason),
Self::FalBillingError(reason) => write!(f, "FalErrorPlus::FalBillingError: {}", reason),
Self::AnyhowError(err) => write!(f, "FalErrorPlus::AnyhowError: {:?}", err),
Self::UrlParseError(err) => write!(f, "FalErrorPlus::UrlParseError: {:?}", err),
Self::UnhandledEndpoint(endpoint) => write!(f, "FalErrorPlus::UnhandledEndpoint: {:?}", endpoint),
@@ -0,0 +1,87 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::gemini_25_flash_image::edit::gemini_25_flash_image_edit;
use fal::prelude::fal_ai::gemini_25_flash_image::edit::Gemini25FlashImageEditInput;
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Gemini25FlashEditArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_urls: Vec<String>,
pub num_images: Gemini25FlashEditNumImages,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum Gemini25FlashEditNumImages {
One,
Two,
Three,
Four,
}
pub async fn enqueue_gemini_25_flash_edit_webhook<R: IntoUrl>(
args: Gemini25FlashEditArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
Gemini25FlashEditNumImages::One => 1,
Gemini25FlashEditNumImages::Two => 2,
Gemini25FlashEditNumImages::Three => 3,
Gemini25FlashEditNumImages::Four => 4,
};
let request = Gemini25FlashImageEditInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
// Constants
num_images: Some(num_images),
output_format: Some("png".to_string()),
};
let result = gemini_25_flash_image_edit(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{enqueue_gemini_25_flash_edit_webhook, Gemini25FlashEditArgs, Gemini25FlashEditNumImages};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Gemini25FlashEditArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
],
num_images: Gemini25FlashEditNumImages::Two,
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly",
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_gemini_25_flash_edit_webhook(args).await?;
Ok(())
}
}
@@ -21,7 +21,7 @@ pub struct GptEditImageByokArgs<'a, V: IntoUrl> {
}
#[derive(Copy, Clone, Debug)]
pub enum GptEditImageSize{
pub enum GptEditImageSize {
Auto,
Square,
Horizontal,
@@ -92,14 +92,14 @@ pub async fn enqueue_gpt_image_1_edit_image_webhook<V: IntoUrl>(
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::creds::open_ai_api_key::OpenAiApiKey;
use crate::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::{enqueue_gpt_image_1_edit_image_webhook, GptEditImageByokArgs, GptEditImageNumImages, GptEditImageQuality, GptEditImageSize};
use crate::requests::webhook::image::edit::enqueue_gpt_image_1_edit_image_webhook::{enqueue_gpt_image_1_edit_image_webhook, GptEditImageByokArgs, GptEditImageNumImages, GptEditImageQuality, GptEditImageSize};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, GRASSY_HILL_TRANSPARENT_IMAGE_URL};
#[tokio::test]
#[ignore]
async fn test_gpt_image_1_edit_image() -> AnyhowResult<()> {
async fn test() -> AnyhowResult<()> {
let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
// XXX: Don't commit secrets!
@@ -0,0 +1,129 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::qwen_image_edit::{qwen_image_edit, QwenImageEditInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct QwenImageEditArgs<'a, U: IntoUrl, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_url: U,
// Request optional
pub num_images: Option<QwenImageEditNumImages>,
pub image_size: Option<QwenImageEditSize>,
pub negative_prompt: Option<String>,
/// Acceleration level for image generation.
/// Options: 'none', 'regular'. Higher acceleration increases speed.
/// 'regular' balances speed and quality. Default value: "none"
pub acceleration: Option<String>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum QwenImageEditNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum QwenImageEditSize {
Square, // 1:1
SquareHd, // 1:1
LandscapeFourByThree, // 4:3
LandscapeSixteenByNine, // 16:9
PortraitThreeByFour, // 3:4
PortraitNineBySixteen, // 9:16
//Custom { width: u32, height: u32 }, // TODO
}
pub async fn enqueue_qwen_image_edit_webhook<U: IntoUrl, R: IntoUrl>(
args: QwenImageEditArgs<'_, U, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = args.num_images
.map(|num| match num {
QwenImageEditNumImages::One => 1,
QwenImageEditNumImages::Two => 2,
QwenImageEditNumImages::Three => 3,
QwenImageEditNumImages::Four => 4,
});
let image_size = args.image_size
.map(|size| match size {
QwenImageEditSize::Square => "square",
QwenImageEditSize::SquareHd => "square_hd",
QwenImageEditSize::LandscapeFourByThree => "landscape_4_3",
QwenImageEditSize::LandscapeSixteenByNine => "landscape_16_9",
QwenImageEditSize::PortraitThreeByFour => "portrait_4_3", // NB: I think they made a mistake
QwenImageEditSize::PortraitNineBySixteen => "portrait_16_9", // NB: I think they made a mistake
})
.map(|size| size.to_string());
let request = QwenImageEditInput {
prompt: args.prompt.to_string(),
image_url: args.image_url.as_str().to_string(),
image_size,
num_images,
// Maybe expose
output_format: Some("png".to_string()), // png or jpeg
negative_prompt: None,
seed: None,
// Constants
//sync_mode: None, // Synchronous / slow
num_inference_steps: None,
guidance_scale: None,
enable_safety_checker: None,
acceleration: None,
};
let result = qwen_image_edit(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_qwen_image_edit_webhook::{enqueue_qwen_image_edit_webhook, QwenImageEditArgs, QwenImageEditNumImages};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::MOUNTAIN_TREE_IMAGE_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = QwenImageEditArgs {
image_url: MOUNTAIN_TREE_IMAGE_URL,
prompt: "put christmas lights on the tree, add snow to the mountains",
num_images: Some(QwenImageEditNumImages::One),
image_size: None,
negative_prompt: None,
acceleration: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_qwen_image_edit_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,67 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::bytedance::seededit::edit_image_v3::seededit_v3_edit;
use fal::endpoints::fal_ai::bytedance::seededit::edit_image_v3::SeedEditV3Input;
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct SeedEditV3EditArgs<'a, U: IntoUrl, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_url: U,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
pub async fn enqueue_seededit_v3_edit_webhook<U: IntoUrl, R: IntoUrl>(
args: SeedEditV3EditArgs<'_, U, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let request = SeedEditV3Input {
prompt: args.prompt.to_string(),
image_url: args.image_url.as_str().to_string(),
// Constants
guidance_scale: None,
enable_safety_checker: None,
};
let result = seededit_v3_edit(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_seededit_v3_edit_webhook::{enqueue_seededit_v3_edit_webhook, SeedEditV3EditArgs};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::MOUNTAIN_TREE_IMAGE_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = SeedEditV3EditArgs {
image_url: MOUNTAIN_TREE_IMAGE_URL,
prompt: "put christmas lights on the tree, add snow to the mountains",
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_seededit_v3_edit_webhook(args).await?;
Ok(())
}
}
@@ -1 +1,5 @@
pub mod enqueue_flux_pro_kontext_max_edit_webhook;
pub mod enqueue_flux_pro_kontext_max_edit_webhook;
pub mod enqueue_gemini_25_flash_edit_webhook;
pub mod enqueue_gpt_image_1_edit_image_webhook;
pub mod enqueue_qwen_image_edit_webhook;
pub mod enqueue_seededit_v3_edit_webhook;
@@ -85,7 +85,7 @@ mod tests {
#[tokio::test]
#[ignore]
async fn test_flux_1_dev() -> AnyhowResult<()> {
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
@@ -84,7 +84,7 @@ mod tests {
#[tokio::test]
#[ignore]
async fn test_flux_1_schnell() -> AnyhowResult<()> {
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
@@ -83,7 +83,7 @@ mod tests {
#[tokio::test]
#[ignore]
async fn test_flux_11_pro() -> AnyhowResult<()> {
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
@@ -96,7 +96,7 @@ mod tests {
#[tokio::test]
#[ignore]
async fn test_gpt_image_1_edit_image() -> AnyhowResult<()> {
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
@@ -3,7 +3,6 @@ pub mod enqueue_flux_1_dev_text_to_image_webhook;
pub mod enqueue_flux_1_schnell_text_to_image_webhook;
pub mod enqueue_flux_pro_11_text_to_image_webhook;
pub mod enqueue_flux_pro_11_ultra_text_to_image_webhook;
pub mod enqueue_gpt_image_1_edit_image_webhook;
pub mod enqueue_gpt_image_1_text_to_image_webhook;
pub mod infill;
pub mod remove_background_rembg_webhook;
@@ -41,7 +41,7 @@ mod tests {
#[tokio::test]
#[ignore] // NB: Manually test, don't run in CI!
async fn test_rembg_request() -> anyhow::Result<()> {
async fn test() -> anyhow::Result<()> {
// XXX: Don't commit secrets!
let api_key = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&api_key);
@@ -5,8 +5,9 @@ use fal::endpoints::fal_ai::kling_video::v1_6::pro::image_to_video::{image_to_vi
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Kling16ProArgs<'a, U: IntoUrl, V: IntoUrl> {
pub struct Kling16ProArgs<'a, U: IntoUrl, T: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub end_frame_image_url: Option<T>,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
@@ -28,8 +29,8 @@ pub enum Kling16ProAspectRatio {
TallNineSixteen, // 9:16
}
pub async fn enqueue_kling_16_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Kling16ProArgs<'_, U, V>
pub async fn enqueue_kling_16_pro_image_to_video_webhook<U: IntoUrl, T: IntoUrl, V: IntoUrl>(
args: Kling16ProArgs<'_, U, T, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Kling16Duration::Default => None,
@@ -45,15 +46,18 @@ pub async fn enqueue_kling_16_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>
let image_url = args.image_url.as_str().to_string();
let tail_image_url = args.end_frame_image_url
.map(|url| url.as_str().to_string());
let request = ProImageToVideoRequest {
image_url,
tail_image_url,
prompt: args.prompt.to_string(),
aspect_ratio,
duration,
// Maybe expose these later
cfg_scale: None,
negative_prompt: None,
tail_image_url: None,
};
let result = image_to_video(request)
@@ -64,35 +68,34 @@ pub async fn enqueue_kling_16_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>
result.map_err(|err| classify_fal_error(err))
}
/*
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_kling_16_image_to_video_webhook::{enqueue_kling_16_image_to_video_webhook, Kling16Args, Kling16Duration};
use crate::requests::webhook::video::enqueue_kling_16_pro_image_to_video_webhook::{enqueue_kling_16_pro_image_to_video_webhook, Kling16Duration, Kling16ProArgs, Kling16ProAspectRatio};
use errors::AnyhowResult;
use std::fs::read_to_string;
use testing::test_file_path::test_file_path;
use test_data::web::image_urls::{JUNO_AT_LAKE_IMAGE_URL, TALL_MOCHI_WITH_GLASSES_IMAGE_URL};
#[tokio::test]
#[ignore]
async fn test_kling16_video() -> AnyhowResult<()> {
let image = test_file_path("test_data/image/juno.jpg")?;
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Kling16Args {
image_path: image,
prompt: "a corgi looks out over the water",
let args = Kling16ProArgs {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
end_frame_image_url: Some(JUNO_AT_LAKE_IMAGE_URL.to_string()),
prompt: "shiba in glasses runs to the lake and stands by the shore",
api_key: &api_key,
duration: Kling16Duration::Default,
aspect_ratio: Kling16ProAspectRatio::WideSixteenNine,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_kling_16_image_to_video_webhook(args).await?;
let result = enqueue_kling_16_pro_image_to_video_webhook(args).await?;
Ok(())
}
}
*/
@@ -5,8 +5,9 @@ use fal::endpoints::fal_ai::kling_video::v2_1::pro::image_to_video::{image_to_vi
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Kling21ProArgs<'a, U: IntoUrl, V: IntoUrl> {
pub struct Kling21ProArgs<'a, U: IntoUrl, T: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub end_frame_image_url: Option<T>,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
@@ -28,8 +29,8 @@ pub enum Kling21ProAspectRatio {
TallNineSixteen, // 9:16
}
pub async fn enqueue_kling_21_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Kling21ProArgs<'_, U, V>
pub async fn enqueue_kling_21_pro_image_to_video_webhook<U: IntoUrl, T: IntoUrl, V: IntoUrl>(
args: Kling21ProArgs<'_, U, T, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Kling21ProDuration::Default => None,
@@ -45,15 +46,18 @@ pub async fn enqueue_kling_21_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>
let image_url = args.image_url.as_str().to_string();
let tail_image_url = args.end_frame_image_url
.map(|url| url.as_str().to_string());
let request = ProImageToVideoRequest {
image_url,
prompt: args.prompt.to_string(),
tail_image_url,
aspect_ratio,
duration,
// Maybe expose these later
cfg_scale: None,
negative_prompt: None,
tail_image_url: None,
};
let result = image_to_video(request)
@@ -64,27 +68,26 @@ pub async fn enqueue_kling_21_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_kling_21_pro_image_to_video_webhook::{enqueue_kling_21_pro_image_to_video_webhook, Kling21ProArgs, Kling21ProAspectRatio, Kling21ProDuration};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{JUNO_AT_LAKE_IMAGE_URL, SUPER_WIDE_FALL_MOUNTAINS_IMAGE_URL};
#[tokio::test]
#[ignore]
async fn test_kling21_pro_video() -> AnyhowResult<()> {
let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Kling21ProArgs {
image_url: image_url,
prompt: "a shot of the mountains, the camera rotates around to show the mountain range",
image_url: SUPER_WIDE_FALL_MOUNTAINS_IMAGE_URL,
end_frame_image_url: Some(JUNO_AT_LAKE_IMAGE_URL.to_string()),
prompt: "a shot of the mountains, the camera pulls back to show a corgi waiting to jump into the lake",
api_key: &api_key,
duration: Kling21ProDuration::Default,
aspect_ratio: Kling21ProAspectRatio::WideSixteenNine,
@@ -6,8 +6,9 @@ use fal::endpoints::fal_ai::bytedance::seedance::v1::lite::image_to_video::Image
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Seedance1LiteArgs<'a, U: IntoUrl, V: IntoUrl> {
pub struct Seedance1LiteArgs<'a, U: IntoUrl, T: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub end_frame_image_url: Option<T>,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
@@ -29,8 +30,8 @@ pub enum Seedance1LiteResolution {
SevenTwentyP, // 720p
}
pub async fn enqueue_seedance_1_lite_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Seedance1LiteArgs<'_, U, V>
pub async fn enqueue_seedance_1_lite_image_to_video_webhook<U: IntoUrl, T: IntoUrl, V: IntoUrl>(
args: Seedance1LiteArgs<'_, U, T, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Seedance1LiteDuration::FiveSeconds => Some("5".to_string()),
@@ -44,14 +45,20 @@ pub async fn enqueue_seedance_1_lite_image_to_video_webhook<U: IntoUrl, V: IntoU
let image_url = args.image_url.as_str().to_string();
let end_image_url = args.end_frame_image_url
.map(|url| url.as_str().to_string());
let request = ImageToVideoRequest {
image_url,
end_image_url,
prompt: args.prompt.to_string(),
duration,
resolution,
// TODO: Add these later
camera_fixed: None,
seed: -1,
// Static
enable_safety_checker: Some(false),
};
let result = image_to_video(request)
@@ -69,20 +76,20 @@ mod tests {
use crate::requests::webhook::video::enqueue_seedance_1_lite_image_to_video_webhook::{enqueue_seedance_1_lite_image_to_video_webhook, Seedance1LiteArgs, Seedance1LiteDuration, Seedance1LiteResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{JUNO_AT_LAKE_IMAGE_URL, TALL_MOCHI_WITH_GLASSES_IMAGE_URL};
#[tokio::test]
#[ignore]
async fn test_kling21_pro_video() -> AnyhowResult<()> {
let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Seedance1LiteArgs {
image_url: image_url,
prompt: "a shot of the mountains as the sun sets and reveals the moon and stars",
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
end_frame_image_url: Some(JUNO_AT_LAKE_IMAGE_URL.to_string()),
prompt: "shiba in glasses runs to the lake and stands by the shore",
api_key: &api_key,
camera_fixed: false,
duration: Seedance1LiteDuration::FiveSeconds,
@@ -0,0 +1,115 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::bytedance::seedance::v1::pro::image_to_video::{seedance_v1_pro_image_to_video, SeedanceImageToVideoProRequest};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Seedance1ProArgs<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub camera_fixed: bool,
pub duration: Seedance1ProDuration,
pub resolution: Seedance1ProResolution,
pub seed: Option<u32>,
}
#[derive(Copy, Clone, Debug)]
pub enum Seedance1ProDuration {
ThreeSeconds,
FourSeconds,
FiveSeconds,
SixSeconds,
SevenSeconds,
EightSeconds,
NineSeconds,
TenSeconds,
ElevenSeconds,
TwelveSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Seedance1ProResolution {
FourEightyP, // 480p
SevenTwentyP, // 720p
TenEightyP, // 1080p
}
pub async fn enqueue_seedance_1_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Seedance1ProArgs<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Seedance1ProDuration::ThreeSeconds => Some("3".to_string()),
Seedance1ProDuration::FourSeconds => Some("4".to_string()),
Seedance1ProDuration::FiveSeconds => Some("5".to_string()),
Seedance1ProDuration::SixSeconds => Some("6".to_string()),
Seedance1ProDuration::SevenSeconds => Some("7".to_string()),
Seedance1ProDuration::EightSeconds => Some("8".to_string()),
Seedance1ProDuration::NineSeconds => Some("9".to_string()),
Seedance1ProDuration::TenSeconds => Some("10".to_string()),
Seedance1ProDuration::ElevenSeconds => Some("11".to_string()),
Seedance1ProDuration::TwelveSeconds => Some("12".to_string()),
};
let resolution = match args.resolution {
Seedance1ProResolution::FourEightyP => Some("480p".to_string()),
Seedance1ProResolution::SevenTwentyP => Some("720p".to_string()),
Seedance1ProResolution::TenEightyP => Some("1080p".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let request = SeedanceImageToVideoProRequest {
image_url,
prompt: args.prompt.to_string(),
duration,
resolution,
// TODO: Add these later
camera_fixed: None,
// Static
enable_safety_checker: Some(false),
};
let result = seedance_v1_pro_image_to_video(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_seedance_1_pro_image_to_video_webhook::{enqueue_seedance_1_pro_image_to_video_webhook, Seedance1ProArgs, Seedance1ProDuration, Seedance1ProResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TALL_MOCHI_WITH_GLASSES_IMAGE_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Seedance1ProArgs {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
prompt: "shiba in glasses runs to the lake and stands by the shore",
api_key: &api_key,
camera_fixed: false,
duration: Seedance1ProDuration::FiveSeconds,
resolution: Seedance1ProResolution::SevenTwentyP,
seed: None,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_seedance_1_pro_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,96 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::veo3::image_to_video_fast::{image_to_video_fast, ImageToVideoFastInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Veo3FastArgs<'a, U: IntoUrl, V: IntoUrl> {
pub prompt: &'a str,
pub image_url: U,
pub duration: Veo3FastDuration,
pub api_key: &'a FalApiKey,
pub resolution: Veo3FastResolution,
pub generate_audio: bool,
pub webhook_url: V,
}
#[derive(Copy, Clone, Debug)]
pub enum Veo3FastDuration {
Default,
EightSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Veo3FastResolution {
Default,
SevenTwentyP,
TenEightyP,
}
pub async fn enqueue_veo_3_fast_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Veo3FastArgs<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Veo3FastDuration::Default => None,
Veo3FastDuration::EightSeconds => Some("8s".to_string()),
};
let resolution= match args.resolution {
Veo3FastResolution::Default => None,
Veo3FastResolution::SevenTwentyP => Some("720p".to_string()),
Veo3FastResolution::TenEightyP => Some("1080p".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let request = ImageToVideoFastInput {
image_url,
prompt: args.prompt.to_string(),
resolution,
duration,
generate_audio: Some(args.generate_audio),
};
let result = image_to_video_fast(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_veo_3_fast_image_to_video_webhook::{enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs, Veo3FastDuration, Veo3FastResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::ERNEST_GHOST_TREX_IMAGE_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
let image_url = ERNEST_GHOST_TREX_IMAGE_URL;
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Veo3FastArgs {
image_url: image_url,
prompt: "man is standing next to a ghost and t-rex, they begin to chase him as the camera pulls back to show the wider scene",
api_key: &api_key,
duration: Veo3FastDuration::EightSeconds,
generate_audio: true,
resolution: Veo3FastResolution::TenEightyP,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_veo_3_fast_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,112 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::veo3::image_to_video::{image_to_video, ImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Veo3Args<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub prompt: &'a str,
pub duration: Veo3Duration,
pub aspect_ratio: Veo3AspectRatio,
pub resolution: Veo3Resolution,
pub generate_audio: bool,
pub api_key: &'a FalApiKey,
pub webhook_url: V,
}
#[derive(Copy, Clone, Debug)]
pub enum Veo3Duration {
Default,
EightSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Veo3AspectRatio {
Default,
WideSixteenNine,
TallNineSixteen,
Square,
}
#[derive(Copy, Clone, Debug)]
pub enum Veo3Resolution {
Default,
SevenTwentyP,
TenEightyP,
}
pub async fn enqueue_veo_3_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Veo3Args<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Veo3Duration::Default => None,
Veo3Duration::EightSeconds => Some("8s".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
Veo3AspectRatio::Default => None,
Veo3AspectRatio::WideSixteenNine => Some("16:9".to_string()),
Veo3AspectRatio::TallNineSixteen => Some("9:16".to_string()),
Veo3AspectRatio::Square => Some("1:1".to_string()),
};
let resolution= match args.resolution {
Veo3Resolution::Default => None,
Veo3Resolution::SevenTwentyP => Some("720p".to_string()),
Veo3Resolution::TenEightyP => Some("1080p".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let request = ImageToVideoInput {
image_url,
prompt: args.prompt.to_string(),
aspect_ratio,
resolution,
duration,
generate_audio: Some(args.generate_audio),
};
let result = image_to_video(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_veo_3_image_to_video_webhook::{enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3AspectRatio, Veo3Duration, Veo3Resolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{JUNO_AT_LAKE_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Veo3Args {
image_url: TREX_SKELETON_IMAGE_URL,
prompt: "the t-rex skeleton starts walking towards the camera and roars",
api_key: &api_key,
duration: Veo3Duration::EightSeconds,
aspect_ratio: Veo3AspectRatio::WideSixteenNine,
resolution: Veo3Resolution::TenEightyP,
generate_audio: true,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_veo_3_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -2,4 +2,7 @@ pub mod enqueue_kling_16_pro_image_to_video_webhook;
pub mod enqueue_kling_21_master_image_to_video_webhook;
pub mod enqueue_kling_21_pro_image_to_video_webhook;
pub mod enqueue_seedance_1_lite_image_to_video_webhook;
pub mod enqueue_veo_2_image_to_video_webhook;
pub mod enqueue_seedance_1_pro_image_to_video_webhook;
pub mod enqueue_veo_2_image_to_video_webhook;
pub mod enqueue_veo_3_fast_image_to_video_webhook;
pub mod enqueue_veo_3_image_to_video_webhook;
@@ -11,6 +11,9 @@ pub enum ApiError {
/// 401. The request was not authorized.
Unauthorized(String),
// 402. Payment required.
PaymentRequired(String),
/// 403. The request was forbidden.
Forbidden(String),
@@ -56,6 +59,7 @@ impl Display for ApiError {
match self {
// Server response code errors
ApiError::InvalidRequest(msg) => write!(f, "Invalid request: {}", msg),
ApiError::PaymentRequired(msg) => write!(f, "Payment required: {}", msg),
ApiError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
ApiError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
ApiError::NotFound(msg) => write!(f, "Not found: {}", msg),
@@ -80,6 +84,13 @@ impl Display for ApiError {
impl From<reqwest::Error> for ApiError {
fn from(error: reqwest::Error) -> Self {
if let Some(status) = error.status() {
let status = status.as_u16();
match status {
402 => return ApiError::PaymentRequired(error.to_string()),
_ => {} // NB: Fallthrough.
}
}
if error.is_timeout() {
ApiError::Timeout(error.to_string())
} else if error.is_connect() {
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::edit::gemini_25_flash_edit_image::{Gemini25FlashEditImageRequest, Gemini25FlashEditImageResponse, GEMINI_25_FLASH_EDIT_IMAGE_PATH};
pub async fn gemini_25_flash_edit_image(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: Gemini25FlashEditImageRequest,
) -> Result<Gemini25FlashEditImageResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
GEMINI_25_FLASH_EDIT_IMAGE_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,2 +1,3 @@
pub mod flux_pro_kontext_max_edit_image;
pub mod gemini_25_flash_edit_image;
pub mod gpt_image_1_edit_image;
@@ -38,6 +38,7 @@ pub async fn filter_bad_response(response: reqwest::Response) -> Result<reqwest:
match status_code {
STATUS_401_UNAUTHORIZED => Err(ApiError::Unauthorized(response_body)),
STATUS_402_PAYMENT_REQUIRED => Err(ApiError::PaymentRequired(response_body)),
STATUS_403_FORBIDDEN => Err(ApiError::Forbidden(response_body)),
STATUS_404_NOT_FOUND => Err(ApiError::NotFound(response_body)),
STATUS_429_TOO_MANY_REQUESTS => Err(ApiError::TooManyRequests(response_body)),
@@ -1,5 +1,6 @@
pub const STATUS_401_UNAUTHORIZED : u16 = 401u16;
pub const STATUS_402_PAYMENT_REQUIRED : u16 = 402u16;
pub const STATUS_403_FORBIDDEN : u16 = 403u16;
pub const STATUS_404_NOT_FOUND : u16 = 404u16;
@@ -15,6 +16,7 @@ mod tests {
#[test]
pub fn test_codes() {
assert_eq!(STATUS_401_UNAUTHORIZED, 401);
assert_eq!(STATUS_402_PAYMENT_REQUIRED, 402);
assert_eq!(STATUS_403_FORBIDDEN, 403);
assert_eq!(STATUS_404_NOT_FOUND, 404);
assert_eq!(STATUS_429_TOO_MANY_REQUESTS, 429);
@@ -0,0 +1,57 @@
use crate::core::commands::enqueue::generate_error::{BillingIssueReason, BillingProvider, GenerateError, ProviderFailureReason};
use crate::core::events::functional_events::show_provider_billing_modal_event::ShowProviderBillingModalEvent;
use enums::common::generation_provider::GenerationProvider;
use log::warn;
use storyteller_client::error::api_error::ApiError;
use storyteller_client::error::storyteller_error::StorytellerError;
use tauri::AppHandle;
pub async fn maybe_notify_frontend_of_billing_errors(
app: &AppHandle,
error: &GenerateError,
) {
match error {
GenerateError::BillingIssue(reason) => {
billing_error(app, reason);
}
GenerateError::ProviderFailure(reason) => {
provider_billing_error(app, reason);
}
_ => {
// Do nothing for other types of errors
}
}
}
fn billing_error(
app: &AppHandle,
reason: &BillingIssueReason,
) {
let provider = match reason.provider {
BillingProvider::Fal => GenerationProvider::Fal,
BillingProvider::Midjourney => GenerationProvider::Midjourney,
BillingProvider::Sora => GenerationProvider::Sora,
BillingProvider::Storyteller => GenerationProvider::Artcraft,
};
warn!("Billing issue with: {:?}", provider);
ShowProviderBillingModalEvent::send_for_provider(provider, app);
}
fn provider_billing_error(
app: &AppHandle,
error: &ProviderFailureReason,
) {
let provider;
match error {
ProviderFailureReason::StorytellerError(StorytellerError::Api(ApiError::PaymentRequired(reason))) => {
warn!("Billing issue with Artcraft/Storyteller: {}", reason);
provider = GenerationProvider::Artcraft;
}
_ => {
return;
}
}
ShowProviderBillingModalEvent::send_for_provider(provider, app);
}
@@ -0,0 +1,2 @@
pub mod notify_frontend_of_errors;
mod maybe_notify_frontend_of_billing_errors;
@@ -0,0 +1,10 @@
use crate::core::commands::enqueue::common::maybe_notify_frontend_of_billing_errors::maybe_notify_frontend_of_billing_errors;
use crate::core::commands::enqueue::generate_error::GenerateError;
use tauri::AppHandle;
pub async fn notify_frontend_of_errors(
app: &AppHandle,
errors: &GenerateError,
) {
maybe_notify_frontend_of_billing_errors(app, errors).await;
}
@@ -0,0 +1,166 @@
use crate::core::artcraft_error::ArtcraftError;
use base64::DecodeError;
use errors::AnyhowError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use midjourney_client::error::midjourney_error::MidjourneyError;
use openai_sora_client::sora_error::SoraError;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum GenerateError {
BadInput(BadInputReason),
MissingCredentials(MissingCredentialsReason),
ProviderFailure(ProviderFailureReason),
/// We couldn't find a provider to dispatch the request to.
NoProviderAvailable,
/// There was a billing, credits, or payments issue.
BillingIssue(BillingIssueReason),
/// The feature is not yet implemented.
NotYetImplemented(String),
// Misc error buckets
AnyhowError(AnyhowError),
DecodeError(DecodeError),
IoError(std::io::Error),
}
#[derive(Debug)]
pub enum BadInputReason {
Base64DecodeError,
BothImageMaskMediaTokenAndBytesSupplied,
CannotDetermineImageMimeType,
InvalidNumberOfInputImages {
provided: u32,
min: u32,
max: u32,
},
InvalidNumberOfRequestedImages {
requested: u32,
min: u32,
max: u32,
},
NoModelSpecified,
RequiredSourceImageMaskNotProvided,
RequiredSourceImageNotProvided,
WrongImageArguments(String),
}
#[derive(Debug)]
pub enum MissingCredentialsReason {
NeedsFalApiKey,
NeedsMidjourneyCredentials,
NeedsMidjourneyUserId,
NeedsMidjourneyUserInfo,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
}
#[derive(Debug)]
pub struct BillingIssueReason {
pub provider: BillingProvider,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum BillingProvider {
Fal,
Midjourney,
Sora,
Storyteller,
}
#[derive(Debug)]
pub enum ProviderFailureReason {
Fal(FalErrorPlus),
MidjourneyError(MidjourneyError),
/// NB: The midjourney client doesn't categorize all errors, so we have to do so on our end.
MidjourneyJobEnqueueFailed,
SoraError(SoraError),
StorytellerError(StorytellerError),
}
impl GenerateError {
//
// Bad input
//
pub fn both_image_mask_media_token_and_bytes_supplied() -> Self {
Self::BadInput(BadInputReason::BothImageMaskMediaTokenAndBytesSupplied)
}
pub fn no_model_specified() -> Self {
Self::BadInput(BadInputReason::NoModelSpecified)
}
pub fn required_source_image_mask_not_provided() -> Self {
Self::BadInput(BadInputReason::RequiredSourceImageMaskNotProvided)
}
pub fn required_source_image_not_provided() -> Self {
Self::BadInput(BadInputReason::RequiredSourceImageNotProvided)
}
//
// Missing credentials
//
pub fn needs_fal_api_key() -> Self {
Self::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey)
}
pub fn needs_midjourney_credentials() -> Self {
Self::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyCredentials)
}
pub fn needs_sora_credentials() -> Self {
Self::MissingCredentials(MissingCredentialsReason::NeedsSoraCredentials)
}
pub fn needs_storyteller_credentials() -> Self {
Self::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials)
}
}
impl From<AnyhowError> for GenerateError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<ArtcraftError> for GenerateError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
ArtcraftError::StorytellerError(e) => Self::ProviderFailure(ProviderFailureReason::StorytellerError(e)),
}
}
}
impl From<FalErrorPlus> for GenerateError {
fn from(value: FalErrorPlus) -> Self {
Self::ProviderFailure(ProviderFailureReason::Fal(value))
}
}
impl From<MidjourneyError> for GenerateError {
fn from(value: MidjourneyError) -> Self {
Self::ProviderFailure(ProviderFailureReason::MidjourneyError(value))
}
}
impl From<SoraError> for GenerateError {
fn from(value: SoraError) -> Self {
Self::ProviderFailure(ProviderFailureReason::SoraError(value))
}
}
impl From<StorytellerError> for GenerateError {
fn from(value: StorytellerError) -> Self {
Self::ProviderFailure(ProviderFailureReason::StorytellerError(value))
}
}
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal::handle_generic_bg_removal;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
@@ -11,8 +11,6 @@ use crate::core::events::generation_events::common::{GenerationAction, Generatio
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
use crate::core::model::image_models::ImageModel;
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::data_dir::trait_data_subdir::DataSubdir;
@@ -126,6 +124,8 @@ pub async fn enqueue_image_bg_removal_command(
match result {
Err(err) => {
error!("Error removing background: {:?}", err);
notify_frontend_of_errors(&app, &err).await;
// TODO: Derive from err. Make service provider optional.
let event = GenerationEnqueueFailureEvent {
@@ -139,7 +139,7 @@ pub async fn enqueue_image_bg_removal_command(
error!("Failed to emit event: {:?}", err); // Fail open.
}
Err(err.to_tauri_response())
Err(error_to_tauri_response(err))
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
@@ -167,7 +167,7 @@ pub async fn handle_request(
storyteller_creds_manager: &StorytellerCredentialManager,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalBgRemovalError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
// TODO(bt,2025-07-07): Other model/provider routing...
@@ -198,3 +198,35 @@ pub async fn handle_request(
Ok(success_event)
}
fn error_to_tauri_response(error: GenerateError) -> CommandErrorResponseWrapper<EnqueueImageBgRemovalErrorType, ()> {
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueImageBgRemovalErrorType::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::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueImageBgRemovalErrorType::NoProviderAvailable;
error_message = "No configured provider available for background removal".to_string();
}
GenerateError::BadInput(BadInputReason::RequiredSourceImageNotProvided) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageBgRemovalErrorType::MissingImage;
error_message = "No image provided for background removal".to_string();
}
GenerateError::BadInput(BadInputReason::Base64DecodeError) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageBgRemovalErrorType::Base64DecodeError;
error_message = "Failed to decode base64 image data".to_string();
}
_ => {}, // Other cases fall through.
}
CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
}
}
@@ -1,71 +0,0 @@
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalErrorType;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use errors::AnyhowError;
use serde::Serialize;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalBgRemovalError {
/// No provider is configured for the model type.
NoProviderAvailable,
/// No storyteller credentials are set.
NeedsStorytellerCredentials,
/// No image was provided for background removal.
MissingImage,
/// Could not decode the base64 image data.
Base64DecodeError,
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
}
impl From<AnyhowError> for InternalBgRemovalError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<StorytellerError> for InternalBgRemovalError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
impl InternalBgRemovalError {
pub fn to_tauri_response<E: Serialize>(&self) -> CommandErrorResponseWrapper<EnqueueImageBgRemovalErrorType, E> {
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueImageBgRemovalErrorType::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 self {
InternalBgRemovalError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueImageBgRemovalErrorType::NoProviderAvailable;
error_message = "No configured provider available for background removal".to_string();
}
InternalBgRemovalError::MissingImage => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageBgRemovalErrorType::MissingImage;
error_message = "No image provided for background removal".to_string();
}
InternalBgRemovalError::Base64DecodeError => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageBgRemovalErrorType::Base64DecodeError;
error_message = "Failed to decode base64 image data".to_string();
}
InternalBgRemovalError::AnyhowError(_) => {}
InternalBgRemovalError::StorytellerError(_) => {}
InternalBgRemovalError::NeedsStorytellerCredentials => {}
}
CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
}
}
}
@@ -1,9 +1,8 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal_artcraft::handle_generic_bg_removal_artcraft;
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal_fal::handle_generic_bg_removal_fal;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
@@ -27,7 +26,7 @@ pub async fn handle_generic_bg_removal(
storyteller_creds_manager: &StorytellerCredentialManager,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalBgRemovalError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let priority = provider_priority_store.get_priority()?;
@@ -65,5 +64,5 @@ pub async fn handle_generic_bg_removal(
}
}
Err(InternalBgRemovalError::NoProviderAvailable)
Err(GenerateError::NoProviderAvailable)
}
@@ -1,13 +1,10 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
use crate::core::model::image_models::ImageModel;
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;
@@ -27,7 +24,6 @@ use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBack
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::GptEditImageQuality;
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
@@ -47,12 +43,12 @@ pub async fn handle_generic_bg_removal_artcraft(
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalBgRemovalError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(InternalBgRemovalError::NeedsStorytellerCredentials);
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials));
},
};
@@ -87,7 +83,7 @@ pub async fn handle_generic_bg_removal_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft background removal: {:?}", err);
return Err(InternalBgRemovalError::StorytellerError(err));
return Err(GenerateError::ProviderFailure(ProviderFailureReason::StorytellerError(err)));
}
};
@@ -104,7 +100,7 @@ async fn upload_image_from_base64_bytes(
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet
) -> Result<MediaFileToken, InternalBgRemovalError> {
) -> Result<MediaFileToken, GenerateError> {
let temp_file;
if let Some(base64_bytes) = &request.base64_image {
@@ -113,10 +109,10 @@ async fn upload_image_from_base64_bytes(
.await
.map_err(|err| {
error!("Failed to save base64 image to temp dir: {:?}", err);
InternalBgRemovalError::Base64DecodeError
GenerateError::BadInput(BadInputReason::Base64DecodeError)
})?;
} else {
return Err(InternalBgRemovalError::MissingImage);
return Err(GenerateError::BadInput(BadInputReason::RequiredSourceImageNotProvided));
};
info!("Uploading image media file from temp file: {:?}", temp_file.path());
@@ -132,7 +128,7 @@ async fn upload_image_from_base64_bytes(
}).await
.map_err(|err| {
error!("Failed to upload image media file: {:?}", err);
InternalBgRemovalError::StorytellerError(err)
GenerateError::ProviderFailure(ProviderFailureReason::StorytellerError(err))
})?;
Ok(result.media_file_token)
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
@@ -14,6 +14,6 @@ pub async fn handle_generic_bg_removal_fal(
app_env_configs: &AppEnvConfigs,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalBgRemovalError> {
unimplemented!("TODO: Implement fal background removal handling");
) -> Result<TaskEnqueueSuccess, GenerateError> {
Err(GenerateError::NotYetImplemented("TODO: Implement fal background removal handling".to_string()))
}
@@ -1,3 +1,2 @@
pub mod enqueue_image_bg_removal_command;
pub mod errors;
pub mod generic;
@@ -1,5 +1,7 @@
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::flux_kontext::handle_flux_kontext_edit::handle_flux_kontext_edit;
use crate::core::commands::enqueue::image_edit::gemini_25_flash::handle_gemini_25_flash_edit::handle_gemini_25_flash_edit;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
@@ -10,8 +12,6 @@ use crate::core::events::generation_events::common::{GenerationAction, Generatio
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
use crate::core::model::image_models::ImageModel;
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::data_dir::trait_data_subdir::DataSubdir;
@@ -27,6 +27,7 @@ use crate::services::storyteller::state::storyteller_credential_manager::Storyte
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_status::TaskStatus;
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};
@@ -41,9 +42,29 @@ use storyteller_client::error::storyteller_error::StorytellerError;
use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Manager, State};
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
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 ContextualImageEditModel {
#[serde(rename = "flux_pro_kontext_max")]
FluxProKontextMax,
#[serde(rename = "gemini_25_flash")]
Gemini25Flash,
#[serde(rename = "gpt_image_1")]
GptImage1,
// #[serde(rename = "qwen")]
// Qwen,
//
// #[serde(rename = "seededit_3")]
// SeedEdit3,
}
#[derive(Deserialize, Debug)]
pub struct EnqueueContextualEditImageCommand {
/// The model to use.
@@ -174,6 +195,8 @@ pub async fn enqueue_contextual_edit_image_command(
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 {
@@ -187,7 +210,7 @@ pub async fn enqueue_contextual_edit_image_command(
error!("Failed to emit event: {:?}", err); // Fail open.
}
Err(err.to_tauri_response())
Err(error_to_tauri_response(err))
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
@@ -217,10 +240,10 @@ pub async fn handle_request(
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let success_event= match request.model {
None => {
return Err(InternalContextualEditImageError::NoModelSpecified)
return Err(GenerateError::no_model_specified())
}
Some(ContextualImageEditModel::FluxProKontextMax) => {
handle_flux_kontext_edit(
@@ -236,6 +259,20 @@ pub async fn handle_request(
sora_task_queue,
).await?
}
Some(ContextualImageEditModel::Gemini25Flash) => {
handle_gemini_25_flash_edit(
request,
app,
app_data_root,
app_env_configs,
provider_priority_store,
storyteller_creds_manager,
fal_creds_manager,
fal_task_queue,
sora_creds_manager,
sora_task_queue,
).await?
}
Some(ContextualImageEditModel::GptImage1) => {
handle_gpt_image_1_edit(
request,
@@ -268,3 +305,41 @@ pub async fn handle_request(
Ok(success_event)
}
fn error_to_tauri_response(error: GenerateError) -> CommandErrorResponseWrapper<EnqueueContextualEditImageErrorType, ()> {
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueContextualEditImageErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.".to_string();
match error {
GenerateError::BadInput(BadInputReason::NoModelSpecified) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueContextualEditImageErrorType::ModelNotSpecified;
error_message = "No model specified for image generation".to_string();
}
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueContextualEditImageErrorType::NoProviderAvailable;
error_message = "No configured provider available for image generation".to_string();
}
GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages { min, max, requested }) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueContextualEditImageErrorType::BadRequest;
error_message = format!("Invalid number of images requested ({}). Must be between {} and {}", requested, min, max);
}
GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages{ min, max, provided }) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueContextualEditImageErrorType::BadRequest;
error_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,
}
}
@@ -1,112 +0,0 @@
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageErrorType;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use errors::AnyhowError;
use openai_sora_client::sora_error::SoraError;
use serde::Serialize;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalContextualEditImageError {
/// Request did not specify a model.
NoModelSpecified,
/// No provider is configured for the model type.
NoProviderAvailable,
/// No storyteller credentials are set.
NeedsStorytellerCredentials,
/// An invalid number of images was requested.
InvalidNumberOfRequestedImages {
min: u32,
max: u32,
requested: u32,
},
/// An invalid number of input images was requested.
InvalidNumberOfInputImages {
min: u32,
max: u32,
requested: u32,
},
// TODO: Maybe just reuse the other error enum variant
/// An invalid number of input images was requested for Flux Kontext.
InvalidNumberOfInputImagesForFluxKontext {
message: String,
},
/// No sora credentials are set.
NeedsSoraCredentials,
SoraError(SoraError),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
}
impl From<AnyhowError> for InternalContextualEditImageError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<SoraError> for InternalContextualEditImageError {
fn from(value: SoraError) -> Self {
Self::SoraError(value)
}
}
impl From<StorytellerError> for InternalContextualEditImageError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
impl InternalContextualEditImageError {
pub fn to_tauri_response<E: Serialize>(&self) -> CommandErrorResponseWrapper<EnqueueContextualEditImageErrorType, E> {
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueContextualEditImageErrorType::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 self {
InternalContextualEditImageError::NoModelSpecified => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueContextualEditImageErrorType::ModelNotSpecified;
error_message = "No model specified for image generation".to_string();
}
InternalContextualEditImageError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueContextualEditImageErrorType::NoProviderAvailable;
error_message = "No configured provider available for image generation".to_string();
}
InternalContextualEditImageError::InvalidNumberOfRequestedImages { min, max, requested } => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueContextualEditImageErrorType::BadRequest;
error_message = format!("Invalid number of images requested ({}). Must be between {} and {}", requested, min, max);
}
InternalContextualEditImageError::InvalidNumberOfInputImages { min, max, requested } => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueContextualEditImageErrorType::BadRequest;
error_message = format!("Invalid number of input images ({}). Must be between {} and {}", requested, min, max);
}
InternalContextualEditImageError::InvalidNumberOfInputImagesForFluxKontext { message } => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueContextualEditImageErrorType::BadRequest;
error_message = message.clone();
}
InternalContextualEditImageError::SoraError(_) => {}
InternalContextualEditImageError::AnyhowError(_) => {}
InternalContextualEditImageError::StorytellerError(_) => {}
InternalContextualEditImageError::NeedsStorytellerCredentials => {}
InternalContextualEditImageError::NeedsSoraCredentials => {}
}
CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
}
}
}
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::flux_kontext::handle_flux_kontext_edit_artcraft::handle_flux_kontext_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
@@ -26,7 +26,7 @@ pub async fn handle_flux_kontext_edit(
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let priority = provider_priority_store.get_priority()?;
@@ -55,5 +55,5 @@ pub async fn handle_flux_kontext_edit(
}
}
Err(InternalContextualEditImageError::NoProviderAvailable)
Err(GenerateError::NoProviderAvailable)
}
@@ -1,11 +1,9 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
use crate::core::model::image_models::ImageModel;
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;
@@ -25,7 +23,6 @@ use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::webhook::image::edit::enqueue_flux_pro_kontext_max_edit_webhook::FluxProKontextMaxNumImages;
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::GptEditImageQuality;
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use storyteller_client::generate::image::edit::flux_pro_kontext_max_edit_image::flux_pro_kontext_max_edit_image;
@@ -42,12 +39,12 @@ pub async fn handle_flux_kontext_edit_artcraft(
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(InternalContextualEditImageError::NeedsStorytellerCredentials);
return Err(GenerateError::needs_storyteller_credentials());
},
};
@@ -62,11 +59,11 @@ pub async fn handle_flux_kontext_edit_artcraft(
Some(3) => Some(FluxProKontextMaxEditImageNumImages::Three),
Some(4) => Some(FluxProKontextMaxEditImageNumImages::Four),
Some(other) => {
return Err(InternalContextualEditImageError::InvalidNumberOfRequestedImages {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
});
}));
},
};
@@ -78,15 +75,13 @@ pub async fn handle_flux_kontext_edit_artcraft(
.unwrap_or(0);
if request.scene_image_media_token.is_some() && list_count > 0 {
return Err(InternalContextualEditImageError::InvalidNumberOfInputImagesForFluxKontext {
message: "Cannot specify both scene_image_media_token and image_media_tokens for Flux Kontext Max".to_string()
});
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(InternalContextualEditImageError::InvalidNumberOfInputImagesForFluxKontext {
message: "Cannot specify more than one image in image_media_tokens for Flux Kontext Max".to_string()
});
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 {
@@ -105,9 +100,8 @@ pub async fn handle_flux_kontext_edit_artcraft(
let media_token = match maybe_media_token {
Some(token) => token,
None => {
return Err(InternalContextualEditImageError::InvalidNumberOfInputImagesForFluxKontext {
message: "No input image specified for Flux Kontext Max edit".to_string(),
});
return Err(GenerateError::BadInput(BadInputReason::WrongImageArguments(
"No input image specified for Flux Kontext Max edit".to_string())));
},
};
@@ -133,7 +127,7 @@ pub async fn handle_flux_kontext_edit_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
return Err(InternalContextualEditImageError::StorytellerError(err));
return Err(GenerateError::from(err));
}
};
@@ -0,0 +1,59 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::gemini_25_flash::handle_gemini_25_flash_edit_artcraft::handle_gemini_25_flash_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use log::info;
use tauri::AppHandle;
pub async fn handle_gemini_25_flash_edit(
request: &EnqueueContextualEditImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
provider_priority_store: &ProviderPriorityStore,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let priority = provider_priority_store.get_priority()?;
// TODO: Check if providers are available before proceeding.
info!("Providers by priority: {:?}", priority);
for provider in priority.iter() {
match provider {
Provider::Fal => {
// Fallthrough (TODO)
}
Provider::Sora => {
// Fallthrough
}
Provider::Artcraft => {
info!("Dispatching gemini 2.5 flash (edit) via Artcraft...");
return handle_gemini_25_flash_edit_artcraft(
request,
app,
app_data_root,
app_env_configs,
storyteller_creds_manager
).await;
}
}
}
Err(GenerateError::NoProviderAvailable)
}
@@ -0,0 +1,119 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{FluxProKontextMaxEditImageNumImages, FluxProKontextMaxEditImageRequest};
use artcraft_api_defs::generate::image::edit::gemini_25_flash_edit_image::{Gemini25FlashEditImageNumImages, Gemini25FlashEditImageRequest};
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use storyteller_client::generate::image::edit::flux_pro_kontext_max_edit_image::flux_pro_kontext_max_edit_image;
use storyteller_client::generate::image::edit::gemini_25_flash_edit_image::gemini_25_flash_edit_image;
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
use tauri::AppHandle;
pub(super) const MAX_IMAGES: usize = 10;
pub async fn handle_gemini_25_flash_edit_artcraft(
request: &EnqueueContextualEditImageCommand,
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 gemini 2.5 flash (edit) ...");
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 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 = Gemini25FlashEditImageRequest {
uuid_idempotency_token,
prompt: Some(request.prompt.clone()),
image_media_tokens: Some(media_tokens),
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 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::Gemini25Flash),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
})
}
@@ -0,0 +1,2 @@
pub (super) mod handle_gemini_25_flash_edit;
pub (super) mod handle_gemini_25_flash_edit_artcraft;
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
@@ -27,7 +27,7 @@ pub async fn handle_gpt_image_1_edit(
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let priority = provider_priority_store.get_priority()?;
@@ -67,5 +67,5 @@ pub async fn handle_gpt_image_1_edit(
}
}
Err(InternalContextualEditImageError::NoProviderAvailable)
Err(GenerateError::NoProviderAvailable)
}
@@ -1,12 +1,10 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::MAX_IMAGES;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
use crate::core::model::image_models::ImageModel;
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;
@@ -24,7 +22,7 @@ use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::GptEditImageQuality;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1_edit_image_webhook::GptEditImageQuality;
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
@@ -40,12 +38,12 @@ pub async fn handle_gpt_image_1_edit_artcraft(
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(InternalContextualEditImageError::NeedsStorytellerCredentials);
return Err(GenerateError::needs_storyteller_credentials());
},
};
@@ -76,11 +74,11 @@ pub async fn handle_gpt_image_1_edit_artcraft(
Some(3) => Some(GptImage1EditImageNumImages::Three),
Some(4) => Some(GptImage1EditImageNumImages::Four),
Some(other) => {
return Err(InternalContextualEditImageError::InvalidNumberOfRequestedImages {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
});
}));
},
};
@@ -95,11 +93,11 @@ pub async fn handle_gpt_image_1_edit_artcraft(
}
if media_tokens.len() > MAX_IMAGES {
return Err(InternalContextualEditImageError::InvalidNumberOfInputImages {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages {
min: 1,
max: MAX_IMAGES as u32,
requested: media_tokens.len() as u32,
});
provided: media_tokens.len() as u32,
}));
}
let request = GptImage1EditImageRequest {
@@ -126,7 +124,7 @@ pub async fn handle_gpt_image_1_edit_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
return Err(InternalContextualEditImageError::StorytellerError(err));
return Err(GenerateError::from(err));
}
};
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageSize, EnqueueContextualEditImageCommand, EnqueueContextualEditImageErrorType};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::MAX_IMAGES;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
@@ -8,8 +8,6 @@ use crate::core::events::functional_events::show_provider_login_modal_event::Sho
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::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
use crate::core::model::image_models::ImageModel;
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::data_dir::trait_data_subdir::DataSubdir;
@@ -54,19 +52,19 @@ pub async fn handle_gpt_image_1_edit_sora(
app_env_configs: &AppEnvConfigs,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
) -> 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(InternalContextualEditImageError::NeedsSoraCredentials);
return Err(GenerateError::needs_sora_credentials());
}
Err(err) => {
error!("Failed to get Sora credentials: {:?}", err);
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Sora, &app);
return Err(InternalContextualEditImageError::AnyhowError(err));
return Err(GenerateError::needs_sora_credentials());
}
};
@@ -81,11 +79,11 @@ pub async fn handle_gpt_image_1_edit_sora(
}
if media_tokens.len() > MAX_IMAGES {
return Err(InternalContextualEditImageError::InvalidNumberOfInputImages {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfInputImages {
min: 1,
max: MAX_IMAGES as u32,
requested: media_tokens.len() as u32,
});
provided: media_tokens.len() as u32,
}));
}
info!("Calling get media file API: {:?}", app_env_configs.storyteller_host);
@@ -1,4 +1,4 @@
pub mod enqueue_contextual_edit_image_command;
pub mod errors;
pub mod gpt_image_1;
mod flux_kontext;
mod flux_kontext;
mod gemini_25_flash;
@@ -1,7 +1,10 @@
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit;
use crate::core::commands::enqueue::image_inpaint::errors::InternalImageInpaintError;
use crate::core::commands::enqueue::image_inpaint::flux_dev_juggernaut_inpaint::handle_flux_dev_juggernaut_inpaint::handle_flux_dev_juggernaut_inpaint;
use crate::core::commands::enqueue::image_inpaint::flux_pro_1_inpaint::handle_flux_pro_1_inpaint::handle_flux_pro_1_inpaint;
use crate::core::commands::enqueue::image_inpaint::flux_pro_kontext_inpaint::handle_flux_pro_kontext_inpaint::handle_flux_pro_kontext_inpaint;
use crate::core::commands::enqueue::image_inpaint::gemini_25_flash_inpaint::handle_gemini_25_flash_inpaint::handle_gemini_25_flash_inpaint;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
@@ -11,8 +14,6 @@ use crate::core::events::generation_events::common::{GenerationAction, Generatio
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
use crate::core::model::image_models::ImageModel;
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::data_dir::trait_data_subdir::DataSubdir;
@@ -44,15 +45,30 @@ use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::commands::enqueue::image_inpaint::flux_dev_juggernaut_inpaint::handle_flux_dev_juggernaut_inpaint::handle_flux_dev_juggernaut_inpaint;
#[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)]
@@ -168,6 +184,8 @@ pub async fn enqueue_image_inpaint_command(
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 {
@@ -181,7 +199,7 @@ pub async fn enqueue_image_inpaint_command(
error!("Failed to emit event: {:?}", err); // Fail open.
}
Err(err.to_tauri_response())
Err(error_to_tauri_response(err))
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
@@ -211,10 +229,10 @@ pub async fn handle_request(
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalImageInpaintError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let success_event= match request.model {
None => {
return Err(InternalImageInpaintError::NoModelSpecified)
return Err(GenerateError::no_model_specified())
}
Some(ImageInpaintModel::FluxDevJuggernaut) => {
handle_flux_dev_juggernaut_inpaint(
@@ -244,6 +262,36 @@ pub async fn handle_request(
sora_task_queue,
).await?
}
Some(ImageInpaintModel::FluxProKontextMax) => {
// TODO: Redirect to contextual image edit handler.
handle_flux_pro_kontext_inpaint(
request,
app,
app_data_root,
app_env_configs,
provider_priority_store,
storyteller_creds_manager,
fal_creds_manager,
fal_task_queue,
sora_creds_manager,
sora_task_queue,
).await?
}
Some(ImageInpaintModel::Gemini25Flash) => {
// TODO: Redirect to contextual image edit handler.
handle_gemini_25_flash_inpaint(
request,
app,
app_data_root,
app_env_configs,
provider_priority_store,
storyteller_creds_manager,
fal_creds_manager,
fal_task_queue,
sora_creds_manager,
sora_task_queue,
).await?
}
};
let result = success_event
@@ -262,3 +310,60 @@ pub async fn handle_request(
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,
}
}
@@ -1,128 +0,0 @@
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageErrorType;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use errors::AnyhowError;
use serde::Serialize;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalImageInpaintError {
/// Request did not specify a model.
NoModelSpecified,
/// No provider is configured for the model type.
NoProviderAvailable,
/// A source image was not provided
NoSourceImageSpecified,
/// A mask image was not provided
NoMaskImageSpecified,
/// Both mask image media token and raw bytes were supplied.
MaskMediaTokenAndBytesSupplied,
/// Mask image has an unknown MIME type.
CouldNotDetermineMaskMimeType,
/// Could not encode the mask image to PNG.
CouldNotEncodeMask,
/// No storyteller credentials are set.
NeedsStorytellerCredentials,
/// An invalid number of images was requested.
InvalidNumberOfRequestedImages {
min: u32,
max: u32,
requested: u32,
},
/// An invalid number of input images was requested.
InvalidNumberOfInputImages {
min: u32,
max: u32,
requested: u32,
},
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
}
impl From<AnyhowError> for InternalImageInpaintError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<StorytellerError> for InternalImageInpaintError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
impl InternalImageInpaintError {
pub fn to_tauri_response<E: Serialize>(&self) -> CommandErrorResponseWrapper<EnqueueInpaintImageErrorType, E> {
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 self {
InternalImageInpaintError::NoModelSpecified => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::ModelNotSpecified;
error_message = "No model specified for image generation".to_string();
}
InternalImageInpaintError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueInpaintImageErrorType::NoProviderAvailable;
error_message = "No configured provider available for image generation".to_string();
}
InternalImageInpaintError::NoSourceImageSpecified => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::NoSourceImageSpecified;
error_message = "No source image was provided".to_string();
}
InternalImageInpaintError::NoMaskImageSpecified => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::NoMaskImageSpecified;
error_message = "No mask image was provided".to_string();
}
InternalImageInpaintError::MaskMediaTokenAndBytesSupplied => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::MultipleMaskImagesSpecified;
error_message = "multiple mask images provided".to_string();
}
InternalImageInpaintError::CouldNotDetermineMaskMimeType => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::BadMaskImage;
error_message = "bad mask image (mime)".to_string();
}
InternalImageInpaintError::CouldNotEncodeMask => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::BadMaskImage;
error_message = "bad mask image (encode)".to_string();
}
InternalImageInpaintError::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);
}
InternalImageInpaintError::InvalidNumberOfInputImages { min, max, requested } => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueInpaintImageErrorType::BadRequest;
error_message = format!("Invalid number of input images ({}). Must be between {} and {}", requested, min, max);
}
InternalImageInpaintError::AnyhowError(_) => {}
InternalImageInpaintError::StorytellerError(_) => {}
InternalImageInpaintError::NeedsStorytellerCredentials => {}
}
CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
}
}
}
@@ -1,9 +1,8 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::image_inpaint::errors::InternalImageInpaintError;
use crate::core::commands::enqueue::image_inpaint::flux_dev_juggernaut_inpaint::handle_flux_dev_juggernaut_inpaint_artcraft::handle_flux_dev_juggernaut_inpaint_artcraft;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
@@ -28,7 +27,7 @@ pub async fn handle_flux_dev_juggernaut_inpaint(
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalImageInpaintError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let priority = provider_priority_store.get_priority()?;
@@ -57,5 +56,5 @@ pub async fn handle_flux_dev_juggernaut_inpaint(
}
}
Err(InternalImageInpaintError::NoProviderAvailable)
Err(GenerateError::NoProviderAvailable)
}
@@ -1,14 +1,11 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::image_inpaint::errors::InternalImageInpaintError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
use crate::core::model::image_models::ImageModel;
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;
@@ -28,7 +25,6 @@ use artcraft_api_defs::generate::image::inpaint::flux_dev_juggernaut_inpaint_ima
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::GptEditImageQuality;
use idempotency::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;
@@ -55,30 +51,22 @@ pub async fn handle_flux_dev_juggernaut_inpaint_artcraft(
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalImageInpaintError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(InternalImageInpaintError::NeedsStorytellerCredentials);
return Err(GenerateError::needs_storyteller_credentials());
},
};
let image_media_token = match &request.image_media_token {
Some(token) => token.clone(),
None => {
return Err(InternalImageInpaintError::NoSourceImageSpecified);
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)
//})?;
let mask_media_token = get_mask(
request,
app_env_configs,
@@ -96,11 +84,11 @@ pub async fn handle_flux_dev_juggernaut_inpaint_artcraft(
Some(3) => Some(FluxDevJuggernautInpaintImageNumImages::Three),
Some(4) => Some(FluxDevJuggernautInpaintImageNumImages::Four),
Some(other) => {
return Err(InternalImageInpaintError::InvalidNumberOfRequestedImages {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
});
}));
},
};
@@ -127,7 +115,7 @@ pub async fn handle_flux_dev_juggernaut_inpaint_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft flux dev juggernaut inpaint: {:?}", err);
return Err(InternalImageInpaintError::StorytellerError(err));
return Err(GenerateError::from(err));
}
};
@@ -143,10 +131,10 @@ async fn get_mask(
request: &EnqueueInpaintImageCommand,
app_env_configs: &AppEnvConfigs,
storyteller_creds: &StorytellerCredentialSet,
) -> Result<MediaFileToken, InternalImageInpaintError> {
) -> Result<MediaFileToken, GenerateError> {
if request.mask_image_media_token.is_some() && request.mask_image_raw_bytes.is_some() {
return Err(InternalImageInpaintError::MaskMediaTokenAndBytesSupplied);
return Err(GenerateError::both_image_mask_media_token_and_bytes_supplied());
}
if let Some(token) = request.mask_image_media_token.as_ref() {
@@ -154,12 +142,12 @@ async fn get_mask(
};
let image_bytes = request.mask_image_raw_bytes.as_ref()
.ok_or(InternalImageInpaintError::NoMaskImageSpecified)?;
.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);
InternalImageInpaintError::CouldNotEncodeMask
GenerateError::AnyhowError(anyhow!("Failed to convert image bytes to png mask"))
})?;
info!("Uploading image media file from bytes...");
@@ -173,7 +161,7 @@ async fn get_mask(
}).await
.map_err(|err| {
error!("Failed to upload image media file: {:?}", err);
InternalImageInpaintError::StorytellerError(err)
GenerateError::from(err)
})?;
Ok(result.media_file_token)
@@ -1,9 +1,8 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::image_inpaint::errors::InternalImageInpaintError;
use crate::core::commands::enqueue::image_inpaint::flux_pro_1_inpaint::handle_flux_pro_1_inpaint_artcraft::handle_flux_pro_1_inpaint_artcraft;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
@@ -28,7 +27,7 @@ pub async fn handle_flux_pro_1_inpaint(
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalImageInpaintError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let priority = provider_priority_store.get_priority()?;
@@ -57,5 +56,5 @@ pub async fn handle_flux_pro_1_inpaint(
}
}
Err(InternalImageInpaintError::NoProviderAvailable)
Err(GenerateError::NoProviderAvailable)
}
@@ -1,14 +1,11 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::image_inpaint::errors::InternalImageInpaintError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
use crate::core::model::image_models::ImageModel;
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;
@@ -28,7 +25,6 @@ use artcraft_api_defs::generate::image::inpaint::flux_pro_1_inpaint_image::{Flux
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::GptEditImageQuality;
use idempotency::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;
@@ -54,30 +50,22 @@ pub async fn handle_flux_pro_1_inpaint_artcraft(
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalImageInpaintError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(InternalImageInpaintError::NeedsStorytellerCredentials);
return Err(GenerateError::needs_storyteller_credentials());
},
};
let image_media_token = match &request.image_media_token {
Some(token) => token.clone(),
None => {
return Err(InternalImageInpaintError::NoSourceImageSpecified);
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)
//})?;
let mask_media_token = get_mask(
request,
app_env_configs,
@@ -95,11 +83,11 @@ pub async fn handle_flux_pro_1_inpaint_artcraft(
Some(3) => Some(FluxPro1InpaintImageNumImages::Three),
Some(4) => Some(FluxPro1InpaintImageNumImages::Four),
Some(other) => {
return Err(InternalImageInpaintError::InvalidNumberOfRequestedImages {
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
min: 1,
max: 4,
requested: other,
});
}));
},
};
@@ -126,13 +114,13 @@ pub async fn handle_flux_pro_1_inpaint_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft flux pro 1 inpaint: {:?}", err);
return Err(InternalImageInpaintError::StorytellerError(err));
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::FluxProKontextMax),
model: Some(GenerationModel::FluxPro1),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageInpaintEdit,
})
@@ -142,10 +130,10 @@ async fn get_mask(
request: &EnqueueInpaintImageCommand,
app_env_configs: &AppEnvConfigs,
storyteller_creds: &StorytellerCredentialSet,
) -> Result<MediaFileToken, InternalImageInpaintError> {
) -> Result<MediaFileToken, GenerateError> {
if request.mask_image_media_token.is_some() && request.mask_image_raw_bytes.is_some() {
return Err(InternalImageInpaintError::MaskMediaTokenAndBytesSupplied);
return Err(GenerateError::both_image_mask_media_token_and_bytes_supplied());
}
if let Some(token) = request.mask_image_media_token.as_ref() {
@@ -153,12 +141,12 @@ async fn get_mask(
};
let image_bytes = request.mask_image_raw_bytes.as_ref()
.ok_or(InternalImageInpaintError::NoMaskImageSpecified)?;
.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);
InternalImageInpaintError::CouldNotEncodeMask
GenerateError::AnyhowError(anyhow!("could not encode image mask to png"))
})?;
info!("Uploading image media file from bytes...");
@@ -172,7 +160,7 @@ async fn get_mask(
}).await
.map_err(|err| {
error!("Failed to upload image media file: {:?}", err);
InternalImageInpaintError::StorytellerError(err)
GenerateError::from(err)
})?;
Ok(result.media_file_token)
@@ -0,0 +1,58 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::image_inpaint::flux_pro_kontext_inpaint::handle_flux_pro_kontext_inpaint_artcraft::handle_flux_pro_kontext_inpaint_artcraft;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use log::info;
use tauri::AppHandle;
pub async fn handle_flux_pro_kontext_inpaint(
request: &EnqueueInpaintImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
provider_priority_store: &ProviderPriorityStore,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let priority = provider_priority_store.get_priority()?;
// TODO: Check if providers are available before proceeding.
info!("Providers by priority: {:?}", priority);
for provider in priority.iter() {
match provider {
Provider::Fal => {
// Fallthrough (for now)
}
Provider::Sora => {
// Fallthrough
}
Provider::Artcraft => {
info!("Dispatching flux pro kontext inpaint via Artcraft...");
return handle_flux_pro_kontext_inpaint_artcraft(
request,
app,
app_data_root,
app_env_configs,
storyteller_creds_manager
).await;
}
}
}
Err(GenerateError::NoProviderAvailable)
}
@@ -0,0 +1,140 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{FluxProKontextMaxEditImageNumImages, FluxProKontextMaxEditImageRequest};
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use artcraft_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 fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use idempotency::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 storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::generate::image::edit::flux_pro_kontext_max_edit_image::flux_pro_kontext_max_edit_image;
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
use storyteller_client::generate::image::generate_flux_1_dev_text_to_image::generate_flux_1_dev_text_to_image;
use storyteller_client::generate::image::generate_flux_1_schnell_text_to_image::generate_flux_1_schnell_text_to_image;
use storyteller_client::generate::image::generate_flux_pro_11_text_to_image::generate_flux_pro_11_text_to_image;
use storyteller_client::generate::image::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
use storyteller_client::generate::image::inpaint::flux_pro_1_inpaint_image::flux_pro_1_inpaint_image;
use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::media_files::upload_image_media_file_from_bytes::{upload_image_media_file_from_bytes, ImageType, UploadImageBytesArgs};
use storyteller_client::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_flux_pro_kontext_inpaint_artcraft(
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,
})
}
@@ -0,0 +1,2 @@
pub (super) mod handle_flux_pro_kontext_inpaint;
pub (super) mod handle_flux_pro_kontext_inpaint_artcraft;
@@ -0,0 +1,59 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::image_inpaint::flux_pro_kontext_inpaint::handle_flux_pro_kontext_inpaint_artcraft::handle_flux_pro_kontext_inpaint_artcraft;
use crate::core::commands::enqueue::image_inpaint::gemini_25_flash_inpaint::handle_gemini_25_flash_inpaint_artcraft::handle_gemini_25_flash_inpaint_artcraft;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use log::info;
use tauri::AppHandle;
pub async fn handle_gemini_25_flash_inpaint(
request: &EnqueueInpaintImageCommand,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
provider_priority_store: &ProviderPriorityStore,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let priority = provider_priority_store.get_priority()?;
// TODO: Check if providers are available before proceeding.
info!("Providers by priority: {:?}", priority);
for provider in priority.iter() {
match provider {
Provider::Fal => {
// Fallthrough (for now)
}
Provider::Sora => {
// Fallthrough
}
Provider::Artcraft => {
info!("Dispatching gemini 25 flash inpaint via Artcraft...");
return handle_gemini_25_flash_inpaint_artcraft(
request,
app,
app_data_root,
app_env_configs,
storyteller_creds_manager
).await;
}
}
}
Err(GenerateError::NoProviderAvailable)
}
@@ -0,0 +1,125 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{FluxProKontextMaxEditImageNumImages, FluxProKontextMaxEditImageRequest};
use artcraft_api_defs::generate::image::edit::gemini_25_flash_edit_image::{Gemini25FlashEditImageNumImages, Gemini25FlashEditImageRequest};
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use artcraft_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 fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use idempotency::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 storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::generate::image::edit::flux_pro_kontext_max_edit_image::flux_pro_kontext_max_edit_image;
use storyteller_client::generate::image::edit::gemini_25_flash_edit_image::gemini_25_flash_edit_image;
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
use storyteller_client::generate::image::generate_flux_1_dev_text_to_image::generate_flux_1_dev_text_to_image;
use storyteller_client::generate::image::generate_flux_1_schnell_text_to_image::generate_flux_1_schnell_text_to_image;
use storyteller_client::generate::image::generate_flux_pro_11_text_to_image::generate_flux_pro_11_text_to_image;
use storyteller_client::generate::image::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
use storyteller_client::generate::image::inpaint::flux_pro_1_inpaint_image::flux_pro_1_inpaint_image;
use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::media_files::upload_image_media_file_from_bytes::{upload_image_media_file_from_bytes, ImageType, UploadImageBytesArgs};
use storyteller_client::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_gemini_25_flash_inpaint_artcraft(
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,
})
}
@@ -0,0 +1,2 @@
pub (super) mod handle_gemini_25_flash_inpaint;
pub (super) mod handle_gemini_25_flash_inpaint_artcraft;
@@ -1,4 +1,5 @@
mod flux_dev_juggernaut_inpaint;
mod flux_pro_1_inpaint;
pub mod enqueue_image_inpaint_command;
pub mod errors;
mod flux_pro_kontext_inpaint;
mod gemini_25_flash_inpaint;
pub mod enqueue_image_inpaint_command;
@@ -1,5 +1,6 @@
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::image_to_object::generic::handle_object::handle_object;
use crate::core::commands::enqueue::image_to_object::internal_object_error::InternalObjectError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
@@ -96,28 +97,30 @@ pub async fn enqueue_image_to_3d_object_command(
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 {
InternalObjectError::NoModelSpecified => {
GenerateError::BadInput(BadInputReason::NoModelSpecified) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageTo3dObjectErrorType::ModelNotSpecified;
error_message = "No model specified for object generation";
}
InternalObjectError::NoProviderAvailable => {
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueImageTo3dObjectErrorType::NoProviderAvailable;
error_message = "No configured provider available for object generation";
}
InternalObjectError::NeedsFalApiKey => {
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey) => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageTo3dObjectErrorType::NeedsFalApiKey;
error_message = "You need to set a FAL api key";
},
InternalObjectError::NeedsStorytellerCredentials => {
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials) => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageTo3dObjectErrorType::NeedsStorytellerCredentials;
error_message = "You need to be logged into Artcraft.";
@@ -159,7 +162,7 @@ pub async fn handle_request(
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalObjectError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let result = handle_object(
request,
@@ -1,7 +1,6 @@
use crate::core::commands::enqueue::image_to_object::enqueue_image_to_3d_object_command::EnqueueImageTo3dObjectRequest;
use crate::core::commands::enqueue::image_to_object::generic::handle_object_artcraft::handle_object_artcraft;
use crate::core::commands::enqueue::image_to_object::generic::handle_object_fal::handle_object_fal;
use crate::core::commands::enqueue::image_to_object::internal_object_error::InternalObjectError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
@@ -10,6 +9,7 @@ use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use tauri::AppHandle;
use crate::core::commands::enqueue::generate_error::GenerateError;
pub async fn handle_object(
request: EnqueueImageTo3dObjectRequest,
@@ -20,7 +20,7 @@ pub async fn handle_object(
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalObjectError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let priority = provider_priority_store.get_priority()?;
@@ -51,5 +51,5 @@ pub async fn handle_object(
}
}
Err(InternalObjectError::NoProviderAvailable)
Err(GenerateError::NoProviderAvailable)
}
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
use crate::core::commands::enqueue::image_to_object::internal_object_error::InternalObjectError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
@@ -25,7 +25,7 @@ pub async fn handle_object_artcraft(
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalObjectError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
@@ -38,7 +38,7 @@ pub async fn handle_object_artcraft(
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalObjectError::NeedsStorytellerCredentials);
return Err(GenerateError::needs_storyteller_credentials());
},
};
@@ -50,7 +50,7 @@ pub async fn handle_object_artcraft(
let job_token = match request.model {
None => {
return Err(InternalObjectError::NoModelSpecified);
return Err(GenerateError::no_model_specified());
}
Some(
EnqueueImageTo3dObjectModel::Hunyuan3d2 |
@@ -74,7 +74,7 @@ pub async fn handle_object_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft Hunyuan 3D 2.0: {:?}", err);
return Err(InternalObjectError::StorytellerError(err));
return Err(GenerateError::from(err));
}
}
}
@@ -97,7 +97,7 @@ pub async fn handle_object_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft Hunyuan 3D 2.1: {:?}", err);
return Err(InternalObjectError::StorytellerError(err));
return Err(GenerateError::from(err));
}
}
}
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
use crate::core::commands::enqueue::image_to_object::internal_object_error::InternalObjectError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
@@ -25,7 +25,7 @@ pub async fn handle_object_fal(
request: EnqueueImageTo3dObjectRequest,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalObjectError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let api_key = match fal_creds_manager.get_key()? {
Some(key) => key,
@@ -38,7 +38,7 @@ pub async fn handle_object_fal(
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalObjectError::NeedsFalApiKey);
return Err(GenerateError::needs_fal_api_key());
},
};
@@ -57,7 +57,7 @@ pub async fn handle_object_fal(
&media_token
).await?;
} else {
return Err(InternalObjectError::AnyhowError(anyhow!("No image media token provided")));
return Err(GenerateError::required_source_image_not_provided());
}
info!("Calling FAL image to 3d ...");
@@ -68,7 +68,7 @@ pub async fn handle_object_fal(
let result = match request.model {
None => {
return Err(InternalObjectError::NoModelSpecified);
return Err(GenerateError::no_model_specified());
}
Some(
EnqueueImageTo3dObjectModel::Hunyuan3d2_0 |
@@ -82,7 +82,7 @@ pub async fn handle_object_fal(
}).await
}
_ => {
return Err(InternalObjectError::AnyhowError(anyhow!("Wrong model specified: {:?}", request.model)));
return Err(GenerateError::AnyhowError(anyhow!("Wrong model specified: {:?}", request.model)));
}
};
@@ -106,7 +106,7 @@ pub async fn handle_object_fal(
if let Err(err) = fal_task_queue.insert(&enqueued) {
error!("Failed to enqueue task: {:?}", err);
return Err(InternalObjectError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
return Err(GenerateError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
}
}
Err(err) => {
@@ -123,7 +123,7 @@ pub async fn handle_object_fal(
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalObjectError::FalError(err));
return Err(GenerateError::from(err));
}
}
@@ -1,62 +0,0 @@
use crate::core::artcraft_error::ArtcraftError;
use base64::DecodeError;
use errors::AnyhowError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalObjectError {
NoModelSpecified,
NoProviderAvailable,
NeedsFalApiKey,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InternalObjectError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<FalErrorPlus> for InternalObjectError {
fn from(value: FalErrorPlus) -> Self {
Self::FalError(value)
}
}
impl From<StorytellerError> for InternalObjectError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
impl From<DecodeError> for InternalObjectError {
fn from(value: DecodeError) -> Self {
Self::DecodeError(value)
}
}
impl From<std::io::Error> for InternalObjectError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
impl From<ArtcraftError> for InternalObjectError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
ArtcraftError::StorytellerError(e) => Self::StorytellerError(e),
_ => Self::ArtcraftError(value),
}
}
}
@@ -1,3 +1,2 @@
pub mod enqueue_image_to_3d_object_command;
pub mod internal_object_error;
mod generic;
@@ -1,12 +1,12 @@
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_to_video::generic::handle_video::handle_video;
use crate::core::commands::enqueue::image_to_video::internal_video_error::InternalVideoError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::model::video_models::VideoModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
@@ -21,16 +21,45 @@ use serde_derive::{Deserialize, Serialize};
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 VideoModel {
#[serde(rename = "kling_1.6_pro")]
Kling16Pro,
#[serde(rename = "kling_2.1_pro")]
Kling21Pro,
#[serde(rename = "kling_2.1_master")]
Kling21Master,
#[serde(rename = "seedance_1.0_lite")]
Seedance10Lite,
#[serde(rename = "veo_2")]
Veo2,
}
#[derive(Deserialize)]
pub struct EnqueueImageToVideoRequest {
/// 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>,
/// REQUIRED.
/// The model to use.
pub model: Option<VideoModel>,
/// Optional text prompt.
/// Currently REQUIRED.
/// Image media file; the starting frame of the video.
/// TODO: In the future we may support base64 images, URLs, or file paths here.
pub image_media_token: Option<MediaFileToken>,
/// Optional.
/// 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 end_frame_image_media_token: Option<MediaFileToken>,
/// Optional.
/// Text prompt used to direct the video.
pub prompt: Option<String>,
}
@@ -88,28 +117,30 @@ pub async fn enqueue_image_to_video_command(
match result {
Err(err) => {
error!("error: {:?}", err);
notify_frontend_of_errors(&app, &err).await;
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueImageToVideoErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
match err {
InternalVideoError::NoModelSpecified => {
GenerateError::BadInput(BadInputReason::NoModelSpecified)=> {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageToVideoErrorType::ModelNotSpecified;
error_message = "No model specified for video generation";
}
InternalVideoError::NoProviderAvailable => {
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueImageToVideoErrorType::NoProviderAvailable;
error_message = "No configured provider available for video generation";
}
InternalVideoError::NeedsFalApiKey => {
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey) => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageToVideoErrorType::NeedsFalApiKey;
error_message = "You need to set a FAL api key";
},
InternalVideoError::NeedsStorytellerCredentials => {
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials) => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageToVideoErrorType::NeedsStorytellerCredentials;
error_message = "You need to be logged into Artcraft.";
@@ -151,7 +182,7 @@ pub async fn handle_request(
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalVideoError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let result = handle_video(
request,
@@ -1,7 +1,7 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
use crate::core::commands::enqueue::image_to_video::generic::handle_video_artcraft::handle_video_artcraft;
use crate::core::commands::enqueue::image_to_video::generic::handle_video_fal::handle_video_fal;
use crate::core::commands::enqueue::image_to_video::internal_video_error::InternalVideoError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
@@ -20,7 +20,7 @@ pub async fn handle_video(
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalVideoError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let priority = provider_priority_store.get_priority()?;
@@ -51,5 +51,5 @@ pub async fn handle_video(
}
}
Err(InternalVideoError::NoProviderAvailable)
Err(GenerateError::NoProviderAvailable)
}
@@ -1,11 +1,10 @@
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
use crate::core::commands::enqueue::image_to_video::internal_video_error::InternalVideoError;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, VideoModel};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::model::video_models::VideoModel;
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;
@@ -35,7 +34,7 @@ pub async fn handle_video_artcraft(
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalVideoError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
@@ -48,7 +47,7 @@ pub async fn handle_video_artcraft(
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalVideoError::NeedsStorytellerCredentials);
return Err(GenerateError::needs_storyteller_credentials());
},
};
@@ -68,7 +67,7 @@ pub async fn handle_video_artcraft(
let job_token = match request.model {
None => {
return Err(InternalVideoError::NoModelSpecified);
return Err(GenerateError::no_model_specified());
}
Some(VideoModel::Kling16Pro) => {
info!("enqueue Kling 1.6 Pro with Artcraft API");
@@ -76,6 +75,7 @@ pub async fn handle_video_artcraft(
let request = GenerateKling16ProImageToVideoRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
end_frame_image_media_token: request.end_frame_image_media_token,
aspect_ratio: Some(GenerateKling16ProAspectRatio::WideSixteenNine),
prompt: request.prompt,
duration: None,
@@ -92,7 +92,7 @@ pub async fn handle_video_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft Kling 1.6 video generation: {:?}", err);
return Err(InternalVideoError::StorytellerError(err));
return Err(GenerateError::from(err));
}
}
}
@@ -118,7 +118,7 @@ pub async fn handle_video_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft Kling 2.1 Master video generation: {:?}", err);
return Err(InternalVideoError::StorytellerError(err));
return Err(GenerateError::from(err));
}
}
}
@@ -128,6 +128,7 @@ pub async fn handle_video_artcraft(
let request = GenerateKling21ProImageToVideoRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
end_frame_image_media_token: request.end_frame_image_media_token,
aspect_ratio: Some(GenerateKling21ProAspectRatio::WideSixteenNine),
prompt: request.prompt,
duration: None,
@@ -144,7 +145,7 @@ pub async fn handle_video_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft Kling 2.1 Pro video generation: {:?}", err);
return Err(InternalVideoError::StorytellerError(err));
return Err(GenerateError::from(err));
}
}
}
@@ -154,6 +155,7 @@ pub async fn handle_video_artcraft(
let request = GenerateSeedance10LiteImageToVideoRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
end_frame_image_media_token: request.end_frame_image_media_token,
prompt: request.prompt,
resolution: None,
duration: None,
@@ -170,7 +172,7 @@ pub async fn handle_video_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft Seedance 1.0 Lite video generation: {:?}", err);
return Err(InternalVideoError::StorytellerError(err));
return Err(GenerateError::from(err));
}
}
}
@@ -196,7 +198,7 @@ pub async fn handle_video_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft Veo 2 video generation: {:?}", err);
return Err(InternalVideoError::StorytellerError(err));
return Err(GenerateError::from(err));
}
}
}
@@ -1,11 +1,10 @@
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
use crate::core::commands::enqueue::image_to_video::internal_video_error::InternalVideoError;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, VideoModel};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::model::video_models::VideoModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
@@ -26,7 +25,7 @@ pub async fn handle_video_fal(
request: EnqueueImageToVideoRequest,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalVideoError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let api_key = match fal_creds_manager.get_key()? {
Some(key) => key,
@@ -39,7 +38,7 @@ pub async fn handle_video_fal(
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalVideoError::NeedsFalApiKey);
return Err(GenerateError::needs_fal_api_key());
},
};
@@ -58,7 +57,7 @@ pub async fn handle_video_fal(
&media_token
).await?;
} else {
return Err(InternalVideoError::AnyhowError(anyhow!("No image media token provided")));
return Err(GenerateError::BadInput(BadInputReason::RequiredSourceImageNotProvided));
}
let filename = temp_download.path().to_path_buf();
@@ -67,7 +66,7 @@ pub async fn handle_video_fal(
let result = match request.model {
None => {
return Err(InternalVideoError::NoModelSpecified);
return Err(GenerateError::no_model_specified());
}
Some(VideoModel::Kling16Pro) => {
info!("enqueue Kling 1.6 image to video with Kling API");
@@ -81,7 +80,7 @@ pub async fn handle_video_fal(
}).await
}
_ => {
return Err(InternalVideoError::AnyhowError(anyhow!("Unsupported model: {:?}", request.model)));
return Err(GenerateError::AnyhowError(anyhow!("Unsupported model: {:?}", request.model)));
}
};
@@ -95,7 +94,7 @@ pub async fn handle_video_fal(
if let Err(err) = fal_task_queue.insert(&enqueued) {
error!("Failed to enqueue task: {:?}", err);
return Err(InternalVideoError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
return Err(GenerateError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
}
}
Err(err) => {
@@ -112,7 +111,7 @@ pub async fn handle_video_fal(
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalVideoError::FalError(err));
return Err(GenerateError::from(err));
}
}
@@ -1,62 +0,0 @@
use crate::core::artcraft_error::ArtcraftError;
use base64::DecodeError;
use errors::AnyhowError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalVideoError {
NoModelSpecified,
NoProviderAvailable,
NeedsFalApiKey,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InternalVideoError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<FalErrorPlus> for InternalVideoError {
fn from(value: FalErrorPlus) -> Self {
Self::FalError(value)
}
}
impl From<StorytellerError> for InternalVideoError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
impl From<DecodeError> for InternalVideoError {
fn from(value: DecodeError) -> Self {
Self::DecodeError(value)
}
}
impl From<std::io::Error> for InternalVideoError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
impl From<ArtcraftError> for InternalVideoError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
ArtcraftError::StorytellerError(e) => Self::StorytellerError(e),
_ => Self::ArtcraftError(value),
}
}
}
@@ -1,3 +1,2 @@
pub mod enqueue_image_to_video_command;
pub mod internal_video_error;
mod generic;
@@ -1,3 +1,5 @@
pub mod common;
pub mod generate_error;
pub mod image_bg_removal;
pub mod image_edit;
pub mod image_inpaint;
@@ -64,6 +64,7 @@ impl TaskEnqueueSuccess{
Some(GenerationModel::FluxPro11) => Some(TaskModelType::FluxPro11),
Some(GenerationModel::FluxPro11Ultra) => Some(TaskModelType::FluxPro11Ultra),
Some(GenerationModel::FluxProKontextMax) => Some(TaskModelType::FluxProKontextMax),
Some(GenerationModel::Gemini25Flash) => Some(TaskModelType::Gemini25Flash),
Some(GenerationModel::GptImage1) => Some(TaskModelType::GptImage1),
Some(GenerationModel::Recraft3) => Some(TaskModelType::Recraft3),
Some(GenerationModel::Kling21Pro) => Some(TaskModelType::Kling21Pro),
@@ -1,9 +1,11 @@
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::gemini_25_flash::handle_gemini_25_flash::handle_gemini_25_flash;
use crate::core::commands::enqueue::text_to_image::generic::handle_image_artcraft::handle_image_artcraft;
use crate::core::commands::enqueue::text_to_image::generic::handle_image_fal::handle_image_fal;
use crate::core::commands::enqueue::text_to_image::gpt_image_1::handle_gpt_image_1::handle_gpt_image_1;
use crate::core::commands::enqueue::text_to_image::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::commands::enqueue::text_to_image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::text_to_image::midjourney::handle_midjourney::handle_midjourney;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
@@ -12,7 +14,6 @@ use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::model::image_models::ImageModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
@@ -26,12 +27,39 @@ use crate::services::storyteller::state::storyteller_credential_manager::Storyte
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use log::{error, info, warn};
use serde::{Deserialize, Serialize};
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
use tauri::{AppHandle, State};
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
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 ImageModel {
#[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 = "gpt_image_1")]
GptImage1,
#[serde(rename = "recraft_3")]
Recraft3,
#[serde(rename = "gemini_25_flash")]
Gemini25Flash,
// Generic Midjourney model, version unknown.
#[serde(rename = "midjourney")]
Midjourney,
}
#[derive(Deserialize)]
pub struct EnqueueTextToImageRequest {
@@ -47,6 +75,13 @@ pub struct EnqueueTextToImageRequest {
/// 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.
@@ -139,22 +174,24 @@ pub async fn enqueue_text_to_image_command(
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 {
InternalImageError::NoModelSpecified => {
GenerateError::BadInput(BadInputReason::NoModelSpecified) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueTextToImageErrorType::ModelNotSpecified;
error_message = "No model specified for image generation";
}
InternalImageError::NoProviderAvailable => {
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueTextToImageErrorType::NoProviderAvailable;
error_message = "No configured provider available for image generation";
}
InternalImageError::NeedsFalApiKey => {
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey) => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueTextToImageErrorType::NeedsFalApiKey;
error_message = "You need to set a FAL api key";
@@ -199,7 +236,7 @@ pub async fn handle_request(
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let result = dispatch_request(
&request,
@@ -249,11 +286,23 @@ pub async fn dispatch_request(
mj_creds_manager: &MidjourneyCredentialManager,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
match request.model {
None => {
return Err(InternalImageError::NoModelSpecified);
return Err(GenerateError::BadInput(BadInputReason::NoModelSpecified));
}
Some(ImageModel::Gemini25Flash) => {
return handle_gemini_25_flash(
request,
app,
app_data_root,
app_env_configs,
provider_priority_store,
storyteller_creds_manager,
sora_creds_manager,
sora_task_queue,
).await;
}
Some(ImageModel::GptImage1) => {
return handle_gpt_image_1(
@@ -297,5 +346,5 @@ pub async fn dispatch_request(
}
}
Err(InternalImageError::NoProviderAvailable)
Err(GenerateError::NoProviderAvailable)
}
@@ -0,0 +1,65 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::text_to_image::gemini_25_flash::handle_gemini_25_flash_artcraft::handle_gemini_25_flash_artcraft;
use crate::core::commands::enqueue::text_to_image::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
use crate::core::commands::enqueue::text_to_image::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
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;
pub async fn handle_gemini_25_flash(
request: &EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
provider_priority_store: &ProviderPriorityStore,
storyteller_creds_manager: &StorytellerCredentialManager,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let priority = provider_priority_store.get_priority()?;
// TODO: Check if providers are available before proceeding.
info!("Providers by priority: {:?}", priority);
for provider in priority.iter() {
match provider {
Provider::Fal => {
// Fallthrough (for now)
}
Provider::Sora => {
// Fallthrough
}
Provider::Artcraft => {
info!("Dispatching gemini 2.5 flash via Artcraft...");
return handle_gemini_25_flash_artcraft(
request,
app_env_configs,
storyteller_creds_manager
).await;
}
}
}
Err(GenerateError::NoProviderAvailable)
}
@@ -0,0 +1,89 @@
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::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::edit::gemini_25_flash_edit_image::{Gemini25FlashEditImageNumImages, Gemini25FlashEditImageRequest};
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use std::time::Duration;
use storyteller_client::generate::image::edit::gemini_25_flash_edit_image::gemini_25_flash_edit_image;
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
use storyteller_client::generate::image::generate_gpt_image_1_text_to_image::generate_gpt_image_1_text_to_image;
use tauri::AppHandle;
pub async fn handle_gemini_25_flash_artcraft(
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials));
},
};
info!("Calling Artcraft gemini 2.5 flash ...");
let uuid_idempotency_token = generate_random_uuid();
let num_images = match request.number_images{
None => None,
Some(1) => Some(Gemini25FlashEditImageNumImages::One),
Some(2) => Some(Gemini25FlashEditImageNumImages::Two),
Some(3) => Some(Gemini25FlashEditImageNumImages::Three),
Some(4) => Some(Gemini25FlashEditImageNumImages::Four),
_ => Some(Gemini25FlashEditImageNumImages::One), // Default to one image if invalid number
};
let request = Gemini25FlashEditImageRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
image_media_tokens: request.image_media_tokens.clone(),
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. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft gemini 2.5 flash: {:?}", err);
return Err(GenerateError::ProviderFailure(ProviderFailureReason::StorytellerError(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,2 @@
pub (crate) mod handle_gemini_25_flash_artcraft;
pub (super) mod handle_gemini_25_flash;
@@ -1,10 +1,9 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::text_to_image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, ImageModel, 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::model::image_models::ImageModel;
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;
@@ -30,7 +29,7 @@ pub async fn handle_image_artcraft(
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
@@ -43,7 +42,7 @@ pub async fn handle_image_artcraft(
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalImageError::NeedsStorytellerCredentials);
return Err(GenerateError::needs_storyteller_credentials());
},
};
@@ -55,16 +54,17 @@ pub async fn handle_image_artcraft(
let job_token = match request.model {
None => {
return Err(InternalImageError::NoModelSpecified);
return Err(GenerateError::BadInput(BadInputReason::NoModelSpecified));
}
Some(
ImageModel::Gemini25Flash |
ImageModel::GptImage1 |
ImageModel::Midjourney
) => {
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: another branch should handle this: {:?}", request.model)));
return Err(GenerateError::AnyhowError(anyhow!("wrong logic: another branch should handle this: {:?}", request.model)));
}
Some(ImageModel::Recraft3) => {
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented in Artcraft")));
return Err(GenerateError::NotYetImplemented(format!("not yet implemented in Artcraft")));
}
Some(ImageModel::Flux1Dev) => {
info!("enqueue Flux 1 Dev");
@@ -101,7 +101,7 @@ pub async fn handle_image_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft Flux 1 Dev text to image generation: {:?}", err);
return Err(InternalImageError::StorytellerError(err));
return Err(GenerateError::from(err));
}
}
}
@@ -140,7 +140,7 @@ pub async fn handle_image_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft Flux 1 Schnell text to image generation: {:?}", err);
return Err(InternalImageError::StorytellerError(err));
return Err(GenerateError::from(err));
}
}
}
@@ -179,7 +179,7 @@ pub async fn handle_image_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft Flux Pro 1.1 text to image generation: {:?}", err);
return Err(InternalImageError::StorytellerError(err));
return Err(GenerateError::from(err));
}
}
}
@@ -218,7 +218,7 @@ pub async fn handle_image_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft Flux Pro 1.1 Ultra text to image generation: {:?}", err);
return Err(InternalImageError::StorytellerError(err));
return Err(GenerateError::from(err));
}
}
}
@@ -1,10 +1,9 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, 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::enqueue::text_to_image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, ImageModel};
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::model::image_models::ImageModel;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use anyhow::anyhow;
@@ -20,7 +19,7 @@ pub async fn handle_image_fal(
request: &EnqueueTextToImageRequest,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let api_key = match fal_creds_manager.get_key()? {
Some(key) => key,
@@ -33,14 +32,14 @@ pub async fn handle_image_fal(
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalImageError::NeedsFalApiKey);
return Err(GenerateError::needs_fal_api_key());
},
};
let api_key = fal_creds_manager.get_key_required()
.map_err(|err| {
error!("EnqueueTextToImage FAL api key required: {:?}", err);
InternalImageError::NeedsFalApiKey
GenerateError::needs_fal_api_key()
})?;
let prompt = request.prompt.as_deref().unwrap_or("");
@@ -49,20 +48,21 @@ pub async fn handle_image_fal(
let result = match request.model {
None => {
return Err(InternalImageError::NoModelSpecified);
return Err(GenerateError::BadInput(BadInputReason::NoModelSpecified));
}
Some(
ImageModel::GptImage1 |
ImageModel::Midjourney
) => {
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: another branch should handle this: {:?}", request.model)));
return Err(GenerateError::AnyhowError(anyhow!("wrong logic: another branch should handle this: {:?}", request.model)));
}
Some(
ImageModel::Flux1Dev |
ImageModel::Gemini25Flash |
ImageModel::Flux1Dev |
ImageModel::Flux1Schnell |
ImageModel::FluxPro11
) => {
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented: {:?}", request.model)));
return Err(GenerateError::NotYetImplemented(format!("not yet implemented: {:?}", request.model)));
}
Some(ImageModel::FluxPro11Ultra) => {
model = GenerationModel::FluxPro11Ultra;
@@ -98,7 +98,7 @@ pub async fn handle_image_fal(
if let Err(err) = fal_task_queue.insert(&enqueued) {
error!("Failed to enqueue task: {:?}", err);
return Err(InternalImageError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
return Err(GenerateError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
}
enqueued
@@ -117,7 +117,7 @@ pub async fn handle_image_fal(
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalImageError::FalError(err));
return Err(GenerateError::from(err));
}
};
@@ -1,15 +1,13 @@
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::text_to_image::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
use crate::core::commands::enqueue::text_to_image::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::commands::enqueue::text_to_image::internal_image_error::InternalImageError;
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::model::image_models::ImageModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
@@ -35,7 +33,7 @@ pub async fn handle_gpt_image_1(
storyteller_creds_manager: &StorytellerCredentialManager,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let priority = provider_priority_store.get_priority()?;
@@ -71,5 +69,5 @@ pub async fn handle_gpt_image_1(
}
}
Err(InternalImageError::NoProviderAvailable)
Err(GenerateError::NoProviderAvailable)
}
@@ -1,12 +1,10 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::text_to_image::internal_image_error::InternalImageError;
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::model::image_models::ImageModel;
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;
@@ -31,12 +29,12 @@ pub async fn handle_gpt_image_1_artcraft(
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(InternalImageError::NeedsStorytellerCredentials);
return Err(GenerateError::needs_storyteller_credentials());
},
};
@@ -102,7 +100,7 @@ pub async fn handle_gpt_image_1_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
return Err(InternalImageError::StorytellerError(err));
return Err(GenerateError::from(err));
}
};
@@ -1,11 +1,10 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::text_to_image::internal_image_error::InternalImageError;
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::core::model::image_models::ImageModel;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use enums::common::generation_provider::GenerationProvider;
@@ -26,19 +25,19 @@ pub async fn handle_gpt_image_1_sora(
app: &AppHandle,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
) -> 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(InternalImageError::NeedsSoraCredentials);
return Err(GenerateError::needs_sora_credentials());
}
Err(err) => {
error!("Error reading Sora credentials: {:?}", err);
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Sora, &app);
return Err(InternalImageError::NeedsSoraCredentials);
return Err(GenerateError::needs_sora_credentials());
}
};
@@ -97,7 +96,7 @@ pub async fn handle_gpt_image_1_sora(
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalImageError::SoraError(err));
return Err(GenerateError::from(err));
}
};
@@ -1,80 +0,0 @@
use crate::core::artcraft_error::ArtcraftError;
use base64::DecodeError;
use errors::AnyhowError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use midjourney_client::error::midjourney_error::MidjourneyError;
use openai_sora_client::sora_error::SoraError;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalImageError {
NoModelSpecified,
NoProviderAvailable,
NeedsFalApiKey,
NeedsMidjourneyCredentials,
NeedsMidjourneyUserInfo,
NeedsMidjourneyUserId,
/// Generic - possibly this came with an error message that we sent as an event.
MidjourneyJobEnqueueFailed,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
FalError(FalErrorPlus),
MidjourneyError(MidjourneyError),
SoraError(SoraError),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InternalImageError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<FalErrorPlus> for InternalImageError {
fn from(value: FalErrorPlus) -> Self {
Self::FalError(value)
}
}
impl From<SoraError> for InternalImageError {
fn from(value: SoraError) -> Self {
Self::SoraError(value)
}
}
impl From<StorytellerError> for InternalImageError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
impl From<DecodeError> for InternalImageError {
fn from(value: DecodeError) -> Self {
Self::DecodeError(value)
}
}
impl From<std::io::Error> for InternalImageError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
impl From<ArtcraftError> for InternalImageError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
ArtcraftError::StorytellerError(e) => Self::StorytellerError(e),
_ => Self::ArtcraftError(value),
}
}
}
@@ -1,15 +1,13 @@
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::text_to_image::internal_image_error::InternalImageError;
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::model::image_models::ImageModel;
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;
@@ -42,19 +40,19 @@ pub async fn handle_midjourney(
request: &EnqueueTextToImageRequest,
app_env_configs: &AppEnvConfigs,
mj_creds_manager: &MidjourneyCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalImageError> {
) -> 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(InternalImageError::NeedsMidjourneyCredentials);
return Err(GenerateError::needs_midjourney_credentials());
}
Err(err) => {
error!("Error reading Midjourney credentials: {:?}", err);
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Midjourney, &app);
return Err(InternalImageError::NeedsMidjourneyCredentials);
return Err(GenerateError::needs_midjourney_credentials());
},
};
@@ -63,11 +61,11 @@ pub async fn handle_midjourney(
let user_info = match mj_creds_manager.maybe_copy_user_info() {
Ok(Some(user_info)) => user_info,
Ok(None) => {
return Err(InternalImageError::NeedsMidjourneyUserInfo);
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserInfo));
}
Err(err) => {
error!("Error reading Midjourney user info: {:?}", err);
return Err(InternalImageError::NeedsMidjourneyUserInfo);
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserInfo));
},
};
@@ -75,7 +73,7 @@ pub async fn handle_midjourney(
Some(user_id) => ChannelId::UserId(user_id),
None => {
error!("Midjourney user info does not contain a user ID.");
return Err(InternalImageError::NeedsMidjourneyUserId);
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserId));
}
};
@@ -98,7 +96,7 @@ pub async fn handle_midjourney(
Ok(result) => result,
Err(err) => {
error!("Failed to use MidJourney: {:?}", err);
return Err(InternalImageError::MidjourneyError(err));
return Err(GenerateError::from(err));
}
};
@@ -123,7 +121,7 @@ pub async fn handle_midjourney(
fn handle_midjourney_errors(
app: &AppHandle,
maybe_errors: Option<Vec<TextToImageError>>
) -> Result<TaskEnqueueSuccess, InternalImageError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
if let Some(errors) = maybe_errors {
if !errors.is_empty() {
let messages: Vec<String> = errors.iter()
@@ -142,5 +140,5 @@ fn handle_midjourney_errors(
}
}
Err(InternalImageError::MidjourneyJobEnqueueFailed)
Err(GenerateError::ProviderFailure(ProviderFailureReason::MidjourneyJobEnqueueFailed))
}
@@ -1,5 +1,5 @@
pub mod enqueue_text_to_image_command;
pub mod generic;
pub mod internal_image_error;
mod gemini_25_flash;
mod gpt_image_1;
mod midjourney;
@@ -1,6 +1,6 @@
use crate::core::events::sendable_event_error::SendableEventError;
use enums::tauri::ux::tauri_event_name::TauriEventName;
use log::info;
use log::{error, info};
use serde::Serialize;
use std::fmt::Debug;
use tauri::{AppHandle, Emitter};
@@ -49,4 +49,11 @@ pub trait BasicSendableEvent : Clone + Debug + Serialize {
}
Ok(())
}
fn send_infallible(&self, app: &AppHandle) {
if let Err(err) = self.send(app) {
// Fail open
error!("Failed to send event `{}`: {:?}", Self::FRONTEND_EVENT_NAME.to_str(), err);
}
}
}
@@ -1,5 +1,6 @@
pub mod canvas_background_removal_complete_event;
pub mod image_edit_complete_event;
pub mod refresh_account_state_event;
pub mod show_provider_billing_modal_event;
pub mod show_provider_login_modal_event;
pub mod text_to_image_generation_complete_event;
@@ -0,0 +1,25 @@
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::ux::tauri_event_name::TauriEventName;
use serde_derive::Serialize;
use tauri::AppHandle;
/// Send a signal to the frontend to show a modal that suggests fixing or setting up
/// service billing requirements.
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct ShowProviderBillingModalEvent {
pub provider: GenerationProvider,
}
impl ShowProviderBillingModalEvent {
pub fn send_for_provider(provider: GenerationProvider, app: &AppHandle) {
let event = Self { provider };
event.send_infallible(&app);
}
}
impl BasicSendableEvent for ShowProviderBillingModalEvent {
const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::ShowProviderBillingModalEvent;
const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Failure;
}
@@ -1,8 +1,6 @@
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
use crate::core::events::sendable_event_error::SendableEventError;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::ux::tauri_event_name::TauriEventName;
use log::error;
use serde_derive::Serialize;
use tauri::AppHandle;
@@ -16,9 +14,7 @@ pub struct ShowProviderLoginModalEvent {
impl ShowProviderLoginModalEvent {
pub fn send_for_provider(provider: GenerationProvider, app: &AppHandle) {
let event = Self { provider };
if let Err(err) = event.send(&app) {
error!("Failed to send ShowProviderLoginModalEvent: {:?}", err); // Fail open
}
event.send_infallible(&app);
}
}
@@ -24,10 +24,14 @@ pub enum GenerationModel {
FluxPro11,
#[serde(rename = "flux_pro_1.1_ultra")]
FluxPro11Ultra,
#[serde(rename = "flux_pro_kontext_max")]
FluxProKontextMax,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "gemini_25_flash")]
Gemini25Flash,
#[serde(rename = "hunyuan_3d_2_0")]
Hunyuan3d2_0,
#[serde(rename = "hunyuan_3d_2_1")]
-1
View File
@@ -2,7 +2,6 @@ pub mod artcraft_error;
pub mod commands;
pub mod events;
pub mod lifecycle;
pub mod model;
pub mod state;
pub mod threads;
pub mod utils;
@@ -1,15 +0,0 @@
use serde_derive::Deserialize;
/// 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 ContextualImageEditModel {
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "flux_pro_kontext_max")]
FluxProKontextMax,
// TODO: Flux Kontext models.
}
@@ -1,24 +0,0 @@
use serde_derive::Deserialize;
/// 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 ImageModel {
#[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 = "gpt_image_1")]
GptImage1,
#[serde(rename = "recraft_3")]
Recraft3,
// Generic Midjourney model, version unknown.
#[serde(rename = "midjourney")]
Midjourney,
}
@@ -1,3 +0,0 @@
pub mod contextual_image_edit_models;
pub mod image_models;
pub mod video_models;
@@ -1,22 +0,0 @@
use serde_derive::Deserialize;
/// 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 VideoModel {
#[serde(rename = "kling_1.6_pro")]
Kling16Pro,
#[serde(rename = "kling_2.1_pro")]
Kling21Pro,
#[serde(rename = "kling_2.1_master")]
Kling21Master,
#[serde(rename = "seedance_1.0_lite")]
Seedance10Lite,
#[serde(rename = "veo_2")]
Veo2,
}
@@ -40,7 +40,6 @@ pub async fn maybe_handle_text_to_image_complete_event(
batch_token,
).await?;
if let Err(err) = event.send(&app) {
error!("Failed to send TextToImageGenerationCompleteEvent: {:?}", err); // Fail open
}
+4 -1
View File
@@ -22,7 +22,7 @@ pub const MOUNTAIN_TREE_IMAGE_URL :&str = "https://cdn-2.fakeyou.com/media/3/4/h
pub const WHITE_HOUSE_SUNSET_IMAGE_URL : &str = "https://cdn-2.fakeyou.com/media/q/t/z/w/k/qtzwka8dd0g2nfyk94k1qh3zzfwqtfhn/asset_qtzwka8dd0g2nfyk94k1qh3zzfwqtfhn.jpg";
// media token: m_z61yg5w7dnnvhb1f9sqt8jf02y61bv
pub const WIDE_FALL_MOUNTAINS_IMAGE_URL : &str = "https://cdn-2.fakeyou.com/media/w/6/m/j/p/w6mjpcqkky7f2f3xkyvkcv3qeedkmejs/asset_w6mjpcqkky7f2f3xkyvkcv3qeedkmejs.jpg";
pub const SUPER_WIDE_FALL_MOUNTAINS_IMAGE_URL: &str = "https://cdn-2.fakeyou.com/media/w/6/m/j/p/w6mjpcqkky7f2f3xkyvkcv3qeedkmejs/asset_w6mjpcqkky7f2f3xkyvkcv3qeedkmejs.jpg";
// media token: m_m1bz02z1kkzanxy6rb4vk1kvq9de9g
pub const JUNO_AT_LAKE_IMAGE_URL : &str = "https://cdn-2.fakeyou.com/media/s/v/c/p/p/svcpp441gyb6mjtt8413kf5zxw1ehy4t/upload_svcpp441gyb6mjtt8413kf5zxw1ehy4t.jpg";
@@ -32,3 +32,6 @@ pub const TALL_MOCHI_WITH_GLASSES_IMAGE_URL : &str = "https://cdn-2.fakeyou.com/
// media token: m_7x18h6qnyay4a42kyyyw2xctasx5k9
pub const TALL_MOCHI_WITH_GLASSES_GLASSES_MASK_IMAGE_URL : &str = "https://cdn-2.fakeyou.com/media/r/e/j/b/p/rejbp61m56hqx4a498xh7j47vkjd7jnh/upload_rejbp61m56hqx4a498xh7j47vkjd7jnh.png";
// media token: m_77cpdyaby60z0qv9g6tv67gw2a1337
pub const TREX_SKELETON_IMAGE_URL : &str = "https://cdn-2.fakeyou.com/media/f/4/j/5/3/f4j53296sc35wqs3csgw4j4vvjf5yyxe/image_f4j53296sc35wqs3csgw4j4vvjf5yyxe.png";
@@ -0,0 +1,50 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GEMINI_25_FLASH_EDIT_IMAGE_PATH: &str = "/v1/generate/image/edit/gemini_25_flash";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Gemini25FlashEditImageRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
// TODO: OneOf for image_urls or image_media_tokens
/// Image media tokens to include in the editing context.
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Number of images to generate. Default is one.
pub num_images: Option<Gemini25FlashEditImageNumImages>,
/// Quality of the image to generate. Default is High.
pub image_quality: Option<Gemini25FlashEditImageImageQuality>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Gemini25FlashEditImageNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Gemini25FlashEditImageImageQuality {
Auto,
Low,
Medium,
High, // Default
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Gemini25FlashEditImageResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1,3 +1,6 @@
pub mod flux_pro_kontext_max_edit_image;
pub mod gpt_image_1_edit_image;
pub mod gemini_25_flash_edit_image;
pub mod qwen_edit_image;
pub mod seededit_3_edit_image;
@@ -0,0 +1,51 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const QWEN_EDIT_IMAGE_PATH: &str = "/v1/generate/image/edit/qwen";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct QwenEditImageRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// The image we're editing.
pub image_media_token: MediaFileToken,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Text prompt to generate the image from.
pub negative_prompt: Option<String>,
/// Acceleration level for image generation.
/// Options: 'none', 'regular'. Higher acceleration increases speed.
/// 'regular' balances speed and quality. Default value: "none"
pub acceleration: Option<QwenEditImageAcceleration>,
/// Number of images to generate. Default is one.
pub num_images: Option<QwenEditImageNumImages>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub enum QwenEditImageAcceleration {
None,
Regular,
High,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum QwenEditImageNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct QwenEditImageResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}

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