Compare commits

...
Author SHA1 Message Date
Brandon Thomas f444a076f7 fix errors 2025-12-20 14:11:41 -05:00
Brandon Thomas 23707f61b2 gpt-image-1.5 and seedream4(.5) all the way through tauri tti
image edit and inpainting still a wip
2025-12-20 13:25:59 -05:00
Brandon Thomas 4000a6b6b6 client 2025-12-20 05:54:20 -05:00
Brandon Thomas 40c6351f05 bytedance endpoints 2025-12-20 05:41:54 -05:00
Brandon Thomas 865129f415 seedream handler 2025-12-20 05:14:33 -05:00
Brandon Thomas 7db3b41bea gpt image 1.5 handler plus helpers 2025-12-20 04:58:36 -05:00
Brandon Thomas a55c7d943c seedream 4/4.5 api defs 2025-12-20 02:15:44 -05:00
Brandon Thomas 526026240a gpt-image-1.5 api def 2025-12-20 02:09:36 -05:00
Brandon Thomas 37bbaaf068 fal client 2025-12-20 01:52:16 -05:00
Brandon Thomas b982bf920c Fal for gpt-image-1.5 2025-12-20 01:21:48 -05:00
43 changed files with 2319 additions and 80 deletions
@@ -1,6 +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 gpt_image_1_edit_image;
pub mod qwen_edit_image;
pub mod seededit_3_edit_image;
@@ -0,0 +1,69 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const BYTEDANCE_SEEDREAM_V4_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/bytedance_seedream_v4";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BytedanceSeedreamV4MultiFunctionImageGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Image media tokens to include in the editing context.
/// If present, we're doing image editing (image-to-image / image-editing)
/// If absent, we're doing image generation (text-to-image)
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Number of images to generate. Default is one.
pub num_images: Option<BytedanceSeedreamV4MultiFunctionImageGenNumImages>,
pub max_images: Option<BytedanceSeedreamV4MultiFunctionImageGenMaxImages>,
pub image_size: Option<BytedanceSeedreamV4MultiFunctionImageGenImageSize>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedreamV4MultiFunctionImageGenNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedreamV4MultiFunctionImageGenMaxImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedreamV4MultiFunctionImageGenImageSize {
// Square
Square,
SquareHd,
// Tall
PortraitFourThree,
PortraitSixteenNine,
// Wide
LandscapeFourThree,
LandscapeSixteenNine,
// Auto
Auto,
Auto2k,
Auto4k,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BytedanceSeedreamV4MultiFunctionImageGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,68 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const BYTEDANCE_SEEDREAM_V4P5_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/bytedance_seedream_v4p5";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BytedanceSeedreamV4p5MultiFunctionImageGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Image media tokens to include in the editing context.
/// If present, we're doing image editing (image-to-image / image-editing)
/// If absent, we're doing image generation (text-to-image)
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Number of images to generate. Default is one.
pub num_images: Option<BytedanceSeedreamV4p5MultiFunctionImageGenNumImages>,
pub max_images: Option<BytedanceSeedreamV4p5MultiFunctionImageGenMaxImages>,
pub image_size: Option<BytedanceSeedreamV4p5MultiFunctionImageGenImageSize>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedreamV4p5MultiFunctionImageGenNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedreamV4p5MultiFunctionImageGenMaxImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedreamV4p5MultiFunctionImageGenImageSize {
// Square
Square,
SquareHd,
// Tall
PortraitFourThree,
PortraitSixteenNine,
// Wide
LandscapeFourThree,
LandscapeSixteenNine,
// Auto
Auto2k,
Auto4k,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BytedanceSeedreamV4p5MultiFunctionImageGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,95 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GPT_IMAGE_1P5_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/gpt_image_1p5";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GptImage1p5MultiFunctionImageGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Image media tokens to include in the editing context (**OR INPAINTING CONTEXT!**).
/// If present, we're doing image editing (image-to-image / image-editing / image-inpainting)
/// If absent, we're doing image generation (text-to-image)
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Only for image editing, which turns this into an inpainting task.
/// Text to image should not set this.
pub mask_image_token: Option<MediaFileToken>,
/// Number of images to generate. Default is one.
pub num_images: Option<GptImage1p5MultiFunctionImageGenNumImages>,
pub image_size: Option<GptImage1p5MultiFunctionImageGenSize>,
pub background: Option<GptImage1p5MultiFunctionImageGenBackground>,
pub quality: Option<GptImage1p5MultiFunctionImageGenQuality>,
/// Only for image editing.
pub input_fidelity: Option<GptImage1p5MultiFunctionImageGenInputFidelity>,
pub output_format: Option<GptImage1p5MultiFunctionImageGenOutputFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GptImage1p5MultiFunctionImageGenNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GptImage1p5MultiFunctionImageGenSize {
/// 1024x1024
Square,
/// 1536x1024
Wide,
/// 1024x1536
Tall,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GptImage1p5MultiFunctionImageGenBackground {
Auto,
Transparent,
Opaque,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GptImage1p5MultiFunctionImageGenQuality {
Low,
Medium,
High,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GptImage1p5MultiFunctionImageGenInputFidelity {
Low,
High,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GptImage1p5MultiFunctionImageGenOutputFormat {
Jpeg,
Png,
Webp,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GptImage1p5MultiFunctionImageGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1 +1,4 @@
pub mod bytedance_seedream_v4_multi_function_image_gen;
pub mod bytedance_seedream_v4p5_multi_function_image_gen;
pub mod gpt_image_1p5_multi_function_image_gen;
pub mod nano_banana_pro_multi_function_image_gen;
@@ -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::multi_function::bytedance_seedream_v4_multi_function_image_gen::{BytedanceSeedreamV4MultiFunctionImageGenRequest, BytedanceSeedreamV4MultiFunctionImageGenResponse, BYTEDANCE_SEEDREAM_V4_MULTI_FUNCTION_IMAGE_GEN_PATH};
pub async fn bytedance_seedream_v4_multi_function_image_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: BytedanceSeedreamV4MultiFunctionImageGenRequest,
) -> Result<BytedanceSeedreamV4MultiFunctionImageGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
BYTEDANCE_SEEDREAM_V4_MULTI_FUNCTION_IMAGE_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -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::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::{BytedanceSeedreamV4p5MultiFunctionImageGenRequest, BytedanceSeedreamV4p5MultiFunctionImageGenResponse, BYTEDANCE_SEEDREAM_V4P5_MULTI_FUNCTION_IMAGE_GEN_PATH};
pub async fn bytedance_seedream_v4p5_multi_function_image_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: BytedanceSeedreamV4p5MultiFunctionImageGenRequest,
) -> Result<BytedanceSeedreamV4p5MultiFunctionImageGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
BYTEDANCE_SEEDREAM_V4P5_MULTI_FUNCTION_IMAGE_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -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::multi_function::gpt_image_1p5_multi_function_image_gen::{GptImage1p5MultiFunctionImageGenRequest, GptImage1p5MultiFunctionImageGenResponse, GPT_IMAGE_1P5_MULTI_FUNCTION_IMAGE_GEN_PATH};
pub async fn gpt_image_1p5_multi_function_image_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GptImage1p5MultiFunctionImageGenRequest,
) -> Result<GptImage1p5MultiFunctionImageGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
GPT_IMAGE_1P5_MULTI_FUNCTION_IMAGE_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -1 +1,4 @@
pub mod bytedance_seedream_v4_multi_function_image_gen_image;
pub mod bytedance_seedream_v4p5_multi_function_image_gen_image;
pub mod gpt_image_1p5_multi_function_image_gen_image;
pub mod nano_banana_pro_multi_function_image_gen_image;
@@ -0,0 +1,182 @@
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::gpt_image::gpt_image_1p5_image_edit::{gpt_image_1p5_image_edit, GptImage1p5ImageEditInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueGptImage1p5EditImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_urls: Vec<String>,
pub num_images: EnqueueGptImage1p5EditImageNumImages,
// Optional args
pub mask_image_url: Option<String>,
pub image_size: Option<EnqueueGptImage1p5EditImageSize>,
pub background: Option<EnqueueGptImage1p5EditImageBackground>,
pub quality: Option<EnqueueGptImage1p5EditImageQuality>,
pub input_fidelity: Option<EnqueueGptImage1p5EditImageInputFidelity>,
pub output_format: Option<EnqueueGptImage1p5EditImageOutputFormat>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5EditImageNumImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5EditImageSize {
/// 1024x1024
Square,
/// 1536x1024
Wide,
/// 1024x1536
Tall,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5EditImageBackground {
Auto,
Transparent,
Opaque,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5EditImageQuality {
Low,
Medium,
High,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5EditImageInputFidelity {
Low,
High,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5EditImageOutputFormat {
Jpeg,
Png,
Webp,
}
pub async fn enqueue_gpt_image_1p5_image_edit_webhook<R: IntoUrl>(
args: EnqueueGptImage1p5EditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
EnqueueGptImage1p5EditImageNumImages::One => 1,
EnqueueGptImage1p5EditImageNumImages::Two => 2,
EnqueueGptImage1p5EditImageNumImages::Three => 3,
EnqueueGptImage1p5EditImageNumImages::Four => 4,
};
let image_size = args.image_size
.map(|s| match s {
EnqueueGptImage1p5EditImageSize::Square => "1024x1024",
EnqueueGptImage1p5EditImageSize::Wide => "1536x1024",
EnqueueGptImage1p5EditImageSize::Tall => "1024x1536",
})
.map(|resolution| resolution.to_string());
let background = args.background
.map(|s| match s {
EnqueueGptImage1p5EditImageBackground::Auto => "auto",
EnqueueGptImage1p5EditImageBackground::Transparent => "transparent",
EnqueueGptImage1p5EditImageBackground::Opaque => "opaque",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let quality = args.quality
.map(|s| match s {
EnqueueGptImage1p5EditImageQuality::Low => "low",
EnqueueGptImage1p5EditImageQuality::Medium => "medium",
EnqueueGptImage1p5EditImageQuality::High => "high",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let input_fidelity = args.input_fidelity
.map(|s| match s {
EnqueueGptImage1p5EditImageInputFidelity::Low => "low",
EnqueueGptImage1p5EditImageInputFidelity::High => "high",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let output_format = args.output_format
.map(|s| match s {
EnqueueGptImage1p5EditImageOutputFormat::Jpeg => "jpeg",
EnqueueGptImage1p5EditImageOutputFormat::Png => "png",
EnqueueGptImage1p5EditImageOutputFormat::Webp => "webp",
})
.map(|aspect_ratio| aspect_ratio.to_string())
.unwrap_or_else(|| "png".to_string());
let request = GptImage1p5ImageEditInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
num_images: Some(num_images),
output_format: Some(output_format),
// Optionals
mask_image_url: args.mask_image_url,
image_size,
background,
quality,
input_fidelity,
};
let result = gpt_image_1p5_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_gpt_image_1p5_edit_image_webhook::{enqueue_gpt_image_1p5_image_edit_webhook, EnqueueGptImage1p5EditImageArgs, EnqueueGptImage1p5EditImageNumImages};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, 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 = EnqueueGptImage1p5EditImageArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly",
num_images: EnqueueGptImage1p5EditImageNumImages::Two,
mask_image_url: None,
image_size: None,
background: None,
quality: None,
input_fidelity: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
output_format: None,
};
let result = enqueue_gpt_image_1p5_image_edit_webhook(args).await?;
Ok(())
}
}
@@ -3,6 +3,7 @@ pub mod enqueue_bytedance_seedream_v4p5_edit_image_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_gpt_image_1p5_edit_image_webhook;
pub mod enqueue_nano_banana_pro_edit_image_webhook;
pub mod enqueue_qwen_image_edit_webhook;
pub mod enqueue_seededit_v3_edit_webhook;
@@ -0,0 +1,164 @@
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::gpt_image::gpt_image_1p5_text_to_image::{gpt_image_1p5_text_to_image, GptImage1p5TextToImageInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueGptImage1p5TextToImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub num_images: EnqueueGptImage1p5TextToImageNumImages,
// Optional args
pub image_size: Option<EnqueueGptImage1p5TextToImageSize>,
pub background: Option<EnqueueGptImage1p5TextToImageBackground>,
pub quality: Option<EnqueueGptImage1p5TextToImageQuality>,
pub output_format: Option<EnqueueGptImage1p5TextToImageOutputFormat>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5TextToImageNumImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5TextToImageSize {
/// 1024x1024
Square,
/// 1536x1024
Wide,
/// 1024x1536
Tall,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5TextToImageBackground {
Auto,
Transparent,
Opaque,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5TextToImageQuality {
Low,
Medium,
High,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5TextToImageOutputFormat {
Jpeg,
Png,
Webp,
}
pub async fn enqueue_gpt_image_1p5_text_to_image_webhook<R: IntoUrl>(
args: EnqueueGptImage1p5TextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
EnqueueGptImage1p5TextToImageNumImages::One => 1,
EnqueueGptImage1p5TextToImageNumImages::Two => 2,
EnqueueGptImage1p5TextToImageNumImages::Three => 3,
EnqueueGptImage1p5TextToImageNumImages::Four => 4,
};
let image_size = args.image_size
.map(|s| match s {
EnqueueGptImage1p5TextToImageSize::Square => "1024x1024",
EnqueueGptImage1p5TextToImageSize::Wide => "1536x1024",
EnqueueGptImage1p5TextToImageSize::Tall => "1024x1536",
})
.map(|resolution| resolution.to_string());
let background = args.background
.map(|s| match s {
EnqueueGptImage1p5TextToImageBackground::Auto => "auto",
EnqueueGptImage1p5TextToImageBackground::Transparent => "transparent",
EnqueueGptImage1p5TextToImageBackground::Opaque => "opaque",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let quality = args.quality
.map(|s| match s {
EnqueueGptImage1p5TextToImageQuality::Low => "low",
EnqueueGptImage1p5TextToImageQuality::Medium => "medium",
EnqueueGptImage1p5TextToImageQuality::High => "high",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let output_format = args.output_format
.map(|s| match s {
EnqueueGptImage1p5TextToImageOutputFormat::Jpeg => "jpeg",
EnqueueGptImage1p5TextToImageOutputFormat::Png => "png",
EnqueueGptImage1p5TextToImageOutputFormat::Webp => "webp",
})
.map(|aspect_ratio| aspect_ratio.to_string())
.unwrap_or_else(|| "png".to_string());
let num_images = match args.num_images {
EnqueueGptImage1p5TextToImageNumImages::One => 1,
EnqueueGptImage1p5TextToImageNumImages::Two => 2,
EnqueueGptImage1p5TextToImageNumImages::Three => 3,
EnqueueGptImage1p5TextToImageNumImages::Four => 4,
};
let request = GptImage1p5TextToImageInput {
prompt: args.prompt.to_string(),
num_images: Some(num_images),
output_format: Some(output_format),
// Optionals
image_size,
background,
quality,
};
let result = gpt_image_1p5_text_to_image(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::text::enqueue_gpt_image_1p5_text_to_image_webhook::{enqueue_gpt_image_1p5_text_to_image_webhook, EnqueueGptImage1p5TextToImageArgs, EnqueueGptImage1p5TextToImageNumImages};
use errors::AnyhowResult;
use std::fs::read_to_string;
#[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 = EnqueueGptImage1p5TextToImageArgs {
prompt: "an anime girl riding on the back of a t-rex",
num_images: EnqueueGptImage1p5TextToImageNumImages::Two,
image_size: None,
background: None,
quality: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
output_format: None,
};
let result = enqueue_gpt_image_1p5_text_to_image_webhook(args).await?;
Ok(())
}
}
@@ -1,3 +1,4 @@
pub mod enqueue_bytedance_seedream_v4_text_to_image_webhook;
pub mod enqueue_bytedance_seedream_v4p5_text_to_image_webhook;
pub mod enqueue_nano_banana_pro_text_to_image_webhook;
pub mod enqueue_nano_banana_pro_text_to_image_webhook;
pub mod enqueue_gpt_image_1p5_text_to_image_webhook;
@@ -71,6 +71,9 @@ impl TaskEnqueueSuccess{
Some(GenerationModel::NanoBanana) => Some(TaskModelType::NanoBanana),
Some(GenerationModel::NanoBananaPro) => Some(TaskModelType::NanoBananaPro),
Some(GenerationModel::GptImage1) => Some(TaskModelType::GptImage1),
Some(GenerationModel::GptImage1p5) => Some(TaskModelType::GptImage1p5),
Some(GenerationModel::Seedream4) => Some(TaskModelType::Seedream4),
Some(GenerationModel::Seedream4p5) => Some(TaskModelType::Seedream4p5),
Some(GenerationModel::GrokImage) => Some(TaskModelType::GrokImage),
Some(GenerationModel::Recraft3) => Some(TaskModelType::Recraft3),
Some(GenerationModel::GrokVideo) => Some(TaskModelType::GrokVideo),
@@ -3,12 +3,14 @@ use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateErr
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
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::gpt_image::handle_gpt_image_1::handle_gpt_image_1;
use crate::core::commands::enqueue::text_to_image::gpt_image::handle_gpt_image_1p5::handle_gpt_image_1p5;
use crate::core::commands::enqueue::text_to_image::grok::handle_grok::handle_grok;
use crate::core::commands::enqueue::text_to_image::midjourney::handle_midjourney::handle_midjourney;
use crate::core::commands::enqueue::text_to_image::nano_banana::handle_nano_banana::handle_nano_banana;
use crate::core::commands::enqueue::text_to_image::nano_banana_pro::handle_nano_banana_pro::handle_nano_banana_pro;
use crate::core::commands::enqueue::text_to_image::seedream::handle_seedream_4_artcraft::handle_seedream_4_artcraft;
use crate::core::commands::enqueue::text_to_image::seedream::handle_seedream_4p5_artcraft::handle_seedream_4p5_artcraft;
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;
@@ -50,19 +52,25 @@ pub enum ImageModel {
FluxPro11,
#[serde(rename = "flux_pro_11_ultra")]
FluxPro11Ultra,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "grok_image")]
GrokImage,
#[serde(rename = "recraft_3")]
Recraft3,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "gpt_image_1p5")]
GptImage1p5,
#[serde(rename = "gemini_25_flash")]
Gemini25Flash,
#[serde(rename = "nano_banana")]
NanoBanana,
#[serde(rename = "nano_banana_pro")]
NanoBananaPro,
#[serde(rename = "seedream_4")]
Seedream4,
#[serde(rename = "seedream_4p5")]
Seedream4p5,
// Generic Midjourney model, version unknown.
#[serde(rename = "midjourney")]
@@ -352,6 +360,15 @@ pub async fn dispatch_request(
sora_task_queue,
).await;
}
Some(ImageModel::GptImage1p5) => {
return handle_gpt_image_1p5(
request,
app,
app_data_root,
app_env_configs,
storyteller_creds_manager,
).await;
}
Some(ImageModel::GrokImage) => {
return handle_grok(
app,
@@ -369,6 +386,20 @@ pub async fn dispatch_request(
mj_creds_manager,
).await;
}
Some(ImageModel::Seedream4) => {
return handle_seedream_4_artcraft(
request,
app_env_configs,
storyteller_creds_manager,
).await;
}
Some(ImageModel::Seedream4p5) => {
return handle_seedream_4p5_artcraft(
request,
app_env_configs,
storyteller_creds_manager,
).await;
}
_ => {
// Fall-through
}
@@ -60,6 +60,9 @@ pub async fn handle_image_artcraft(
ImageModel::NanoBanana |
ImageModel::NanoBananaPro |
ImageModel::GptImage1 |
ImageModel::GptImage1p5 |
ImageModel::Seedream4 |
ImageModel::Seedream4p5 |
ImageModel::GrokImage |
ImageModel::Midjourney
) => {
@@ -3,8 +3,8 @@ use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_
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::gpt_image::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
use crate::core::commands::enqueue::text_to_image::gpt_image::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;
@@ -0,0 +1,25 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
use crate::core::commands::enqueue::text_to_image::gpt_image::handle_gpt_image_1p5_artcraft::handle_gpt_image_1p5_artcraft;
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 tauri::AppHandle;
pub async fn handle_gpt_image_1p5(
request: &EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, GenerateError> {
handle_gpt_image_1p5_artcraft(
request,
app_env_configs,
storyteller_creds_manager
).await
}
@@ -0,0 +1,104 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{GptImage1p5MultiFunctionImageGenNumImages, GptImage1p5MultiFunctionImageGenQuality, GptImage1p5MultiFunctionImageGenRequest, GptImage1p5MultiFunctionImageGenSize};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use std::time::Duration;
use storyteller_client::endpoints::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen_image::gpt_image_1p5_multi_function_image_gen;
use tauri::AppHandle;
pub async fn handle_gpt_image_1p5_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::needs_storyteller_credentials());
},
};
info!("Calling Artcraft gpt-image-1.5 ...");
let uuid_idempotency_token = generate_random_uuid();
let image_size = request.aspect_ratio
.map(|size| match size {
TextToImageSize::Auto => GptImage1p5MultiFunctionImageGenSize::Square,
TextToImageSize::Square => GptImage1p5MultiFunctionImageGenSize::Square,
TextToImageSize::Tall => GptImage1p5MultiFunctionImageGenSize::Tall,
TextToImageSize::Wide => GptImage1p5MultiFunctionImageGenSize::Wide,
});
let num_images = match request.number_images {
None => None,
Some(1) => Some(GptImage1p5MultiFunctionImageGenNumImages::One),
Some(2) => Some(GptImage1p5MultiFunctionImageGenNumImages::Two),
Some(3) => Some(GptImage1p5MultiFunctionImageGenNumImages::Three),
Some(4) => Some(GptImage1p5MultiFunctionImageGenNumImages::Four),
// TODO: Error
//Some(other) => {
// return Err(InternalImageError::InvalidNumberOfRequestedImages {
// min: 1,
// max: 4,
// requested: other,
// });
//},
_ => Some(GptImage1p5MultiFunctionImageGenNumImages::One), // Default to one image if invalid number
};
let request = GptImage1p5MultiFunctionImageGenRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
image_size,
num_images,
// Not provided for text-to-image
image_media_tokens: None,
mask_image_token: None,
input_fidelity: None,
// Not provided
output_format: None,
background: None,
quality: None,
};
let result = gpt_image_1p5_multi_function_image_gen(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft gpt-image-1.5. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft gpt-image-1.5: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::GptImage1p5),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
})
}
@@ -1,3 +1,5 @@
pub (crate) mod handle_gpt_image_1_artcraft;
pub (crate) mod handle_gpt_image_1_sora;
pub (super) mod handle_gpt_image_1;
pub (super) mod handle_gpt_image_1p5;
pub (super) mod handle_gpt_image_1p5_artcraft;
@@ -2,6 +2,7 @@ pub mod enqueue_text_to_image_command;
pub mod generic;
mod nano_banana;
mod nano_banana_pro;
mod gpt_image_1;
mod gpt_image;
mod midjourney;
mod grok;
mod grok;
mod seedream;
@@ -3,8 +3,6 @@ use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_
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::nano_banana::handle_nano_banana_artcraft::handle_nano_banana_artcraft;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
@@ -0,0 +1,100 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::{BytedanceSeedreamV4MultiFunctionImageGenImageSize, BytedanceSeedreamV4MultiFunctionImageGenNumImages, BytedanceSeedreamV4MultiFunctionImageGenRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use std::time::Duration;
use storyteller_client::endpoints::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen_image::bytedance_seedream_v4_multi_function_image_gen;
use tauri::AppHandle;
pub async fn handle_seedream_4_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::needs_storyteller_credentials());
},
};
info!("Calling Artcraft seedream 4 ...");
let uuid_idempotency_token = generate_random_uuid();
let image_size = request.aspect_ratio
.map(|size| match size {
TextToImageSize::Auto => BytedanceSeedreamV4MultiFunctionImageGenImageSize::Square,
TextToImageSize::Square => BytedanceSeedreamV4MultiFunctionImageGenImageSize::Square,
TextToImageSize::Tall => BytedanceSeedreamV4MultiFunctionImageGenImageSize::PortraitSixteenNine,
TextToImageSize::Wide => BytedanceSeedreamV4MultiFunctionImageGenImageSize::LandscapeSixteenNine,
});
let num_images = match request.number_images {
None => None,
Some(1) => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::One),
Some(2) => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Two),
Some(3) => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Three),
Some(4) => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Four),
// TODO: Error
//Some(other) => {
// return Err(InternalImageError::InvalidNumberOfRequestedImages {
// min: 1,
// max: 4,
// requested: other,
// });
//},
_ => Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::One), // Default to one image if invalid number
};
let request = BytedanceSeedreamV4MultiFunctionImageGenRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
num_images,
image_size,
// Not provided for text-to-image
image_media_tokens: None,
// Not provided
max_images: None,
};
let result = bytedance_seedream_v4_multi_function_image_gen(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft seedream 4. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft seedream 4: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::Seedream4p5),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
})
}
@@ -0,0 +1,100 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::{BytedanceSeedreamV4p5MultiFunctionImageGenImageSize, BytedanceSeedreamV4p5MultiFunctionImageGenNumImages, BytedanceSeedreamV4p5MultiFunctionImageGenRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use idempotency::uuid::generate_random_uuid;
use log::{error, info};
use std::time::Duration;
use storyteller_client::endpoints::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen_image::bytedance_seedream_v4p5_multi_function_image_gen;
use tauri::AppHandle;
pub async fn handle_seedream_4p5_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::needs_storyteller_credentials());
},
};
info!("Calling Artcraft seedream 4.5 ...");
let uuid_idempotency_token = generate_random_uuid();
let image_size = request.aspect_ratio
.map(|size| match size {
TextToImageSize::Auto => BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::Square,
TextToImageSize::Square => BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::Square,
TextToImageSize::Tall => BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::PortraitSixteenNine,
TextToImageSize::Wide => BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::LandscapeSixteenNine,
});
let num_images = match request.number_images {
None => None,
Some(1) => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::One),
Some(2) => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Two),
Some(3) => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Three),
Some(4) => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Four),
// TODO: Error
//Some(other) => {
// return Err(InternalImageError::InvalidNumberOfRequestedImages {
// min: 1,
// max: 4,
// requested: other,
// });
//},
_ => Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::One), // Default to one image if invalid number
};
let request = BytedanceSeedreamV4p5MultiFunctionImageGenRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
num_images,
image_size,
// Not provided for text-to-image
image_media_tokens: None,
// Not provided
max_images: None,
};
let result = bytedance_seedream_v4p5_multi_function_image_gen(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
let job_id = match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft seedream 4.5. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft seedream 4.5: {:?}", err);
return Err(GenerateError::from(err));
}
};
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::Seedream4p5),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
})
}
@@ -0,0 +1,2 @@
pub (super) mod handle_seedream_4_artcraft;
pub (super) mod handle_seedream_4p5_artcraft;
@@ -31,6 +31,8 @@ pub enum GenerationModel {
FluxProKontextMax,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "gpt_image_1p5")]
GptImage1p5,
#[serde(rename = "grok_image")]
GrokImage,
#[serde(rename = "gemini_25_flash")]
@@ -39,6 +41,10 @@ pub enum GenerationModel {
NanoBanana,
#[serde(rename = "nano_banana_pro")]
NanoBananaPro,
#[serde(rename = "seedream_4")]
Seedream4,
#[serde(rename = "seedream_4p5")]
Seedream4p5,
#[serde(rename = "hunyuan_3d_2_0")]
Hunyuan3d2_0,
@@ -29,6 +29,8 @@ pub enum ModelType {
FluxProKontextMax,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "gpt_image_1p5")]
GptImage1p5,
// Generic grok image model without a version
#[serde(rename = "grok_image")]
GrokImage,
@@ -45,6 +47,10 @@ pub enum ModelType {
NanoBanana,
#[serde(rename = "nano_banana_pro")]
NanoBananaPro,
#[serde(rename = "seedream_4")]
Seedream4,
#[serde(rename = "seedream_4p5")]
Seedream4p5,
/// Midjourney without distinguishing a model type or version
#[serde(rename = "midjourney")]
@@ -117,6 +123,7 @@ impl ModelType {
Self::FluxPro11Ultra => "flux_pro_1p1_ultra",
Self::FluxProKontextMax => "flux_pro_kontext_max",
Self::GptImage1 => "gpt_image_1",
Self::GptImage1p5 => "gpt_image_1p5",
Self::GrokImage => "grok_image",
Self::Recraft3 => "recraft_3",
Self::SeedEdit3 => "seededit_3",
@@ -124,6 +131,8 @@ impl ModelType {
Self::Gemini25Flash => "gemini_25_flash",
Self::NanoBanana => "nano_banana",
Self::NanoBananaPro => "nano_banana_pro",
Self::Seedream4 => "seedream_4",
Self::Seedream4p5 => "seedream_4p5",
Self::Midjourney => "midjourney",
Self::MidjourneyV6 => "midjourney_v6",
Self::MidjourneyV6p1 => "midjourney_v6p1",
@@ -162,6 +171,7 @@ impl ModelType {
"flux_pro_1p1_ultra" => Ok(Self::FluxPro11Ultra),
"flux_pro_kontext_max" => Ok(Self::FluxProKontextMax),
"gpt_image_1" => Ok(Self::GptImage1),
"gpt_image_1p5" => Ok(Self::GptImage1p5),
"grok_image" => Ok(Self::GrokImage),
"recraft_3" => Ok(Self::Recraft3),
"seededit_3" => Ok(Self::SeedEdit3),
@@ -169,6 +179,8 @@ impl ModelType {
"gemini_25_flash" => Ok(Self::Gemini25Flash),
"nano_banana" => Ok(Self::NanoBanana),
"nano_banana_pro" => Ok(Self::NanoBananaPro),
"seedream_4" => Ok(Self::Seedream4),
"seedream_4p5" => Ok(Self::Seedream4p5),
"midjourney" => Ok(Self::Midjourney),
"midjourney_v6" => Ok(Self::MidjourneyV6),
"midjourney_v6p1" => Ok(Self::MidjourneyV6p1),
@@ -211,6 +223,7 @@ impl ModelType {
Self::FluxPro11Ultra,
Self::FluxProKontextMax,
Self::GptImage1,
Self::GptImage1p5,
Self::GrokImage,
Self::Recraft3,
Self::SeedEdit3,
@@ -218,6 +231,8 @@ impl ModelType {
Self::Gemini25Flash,
Self::NanoBanana,
Self::NanoBananaPro,
Self::Seedream4,
Self::Seedream4p5,
Self::Midjourney,
Self::MidjourneyV6,
Self::MidjourneyV6p1,
@@ -265,6 +280,7 @@ mod tests {
assert_serialization(ModelType::FluxPro11Ultra, "flux_pro_1p1_ultra");
assert_serialization(ModelType::FluxProKontextMax, "flux_pro_kontext_max");
assert_serialization(ModelType::GptImage1, "gpt_image_1");
assert_serialization(ModelType::GptImage1p5, "gpt_image_1p5");
assert_serialization(ModelType::GrokImage, "grok_image");
assert_serialization(ModelType::Recraft3, "recraft_3");
assert_serialization(ModelType::SeedEdit3, "seededit_3");
@@ -272,6 +288,8 @@ mod tests {
assert_serialization(ModelType::Gemini25Flash, "gemini_25_flash");
assert_serialization(ModelType::NanoBanana, "nano_banana");
assert_serialization(ModelType::NanoBananaPro, "nano_banana_pro");
assert_serialization(ModelType::Seedream4, "seedream_4");
assert_serialization(ModelType::Seedream4p5, "seedream_4p5");
assert_serialization(ModelType::Midjourney, "midjourney");
assert_serialization(ModelType::MidjourneyV6, "midjourney_v6");
assert_serialization(ModelType::MidjourneyV6p1, "midjourney_v6p1");
@@ -307,6 +325,7 @@ mod tests {
assert_eq!(ModelType::FluxPro11Ultra.to_str(), "flux_pro_1p1_ultra");
assert_eq!(ModelType::FluxProKontextMax.to_str(), "flux_pro_kontext_max");
assert_eq!(ModelType::GptImage1.to_str(), "gpt_image_1");
assert_eq!(ModelType::GptImage1p5.to_str(), "gpt_image_1p5");
assert_eq!(ModelType::GrokImage.to_str(), "grok_image");
assert_eq!(ModelType::Recraft3.to_str(), "recraft_3");
assert_eq!(ModelType::SeedEdit3.to_str(), "seededit_3");
@@ -314,6 +333,8 @@ mod tests {
assert_eq!(ModelType::Gemini25Flash.to_str(), "gemini_25_flash");
assert_eq!(ModelType::NanoBanana.to_str(), "nano_banana");
assert_eq!(ModelType::NanoBananaPro.to_str(), "nano_banana_pro");
assert_eq!(ModelType::Seedream4.to_str(), "seedream_4");
assert_eq!(ModelType::Seedream4p5.to_str(), "seedream_4p5");
assert_eq!(ModelType::Midjourney.to_str(), "midjourney");
assert_eq!(ModelType::MidjourneyV6.to_str(), "midjourney_v6");
assert_eq!(ModelType::MidjourneyV6p1.to_str(), "midjourney_v6p1");
@@ -351,6 +372,7 @@ mod tests {
assert_eq!(ModelType::from_str("flux_pro_1p1_ultra").unwrap(), ModelType::FluxPro11Ultra);
assert_eq!(ModelType::from_str("flux_pro_kontext_max").unwrap(), ModelType::FluxProKontextMax);
assert_eq!(ModelType::from_str("gpt_image_1").unwrap(), ModelType::GptImage1);
assert_eq!(ModelType::from_str("gpt_image_1p5").unwrap(), ModelType::GptImage1p5);
assert_eq!(ModelType::from_str("grok_image").unwrap(), ModelType::GrokImage);
assert_eq!(ModelType::from_str("recraft_3").unwrap(), ModelType::Recraft3);
assert_eq!(ModelType::from_str("seededit_3").unwrap(), ModelType::SeedEdit3);
@@ -358,6 +380,8 @@ mod tests {
assert_eq!(ModelType::from_str("gemini_25_flash").unwrap(), ModelType::Gemini25Flash);
assert_eq!(ModelType::from_str("nano_banana").unwrap(), ModelType::NanoBanana);
assert_eq!(ModelType::from_str("nano_banana_pro").unwrap(), ModelType::NanoBananaPro);
assert_eq!(ModelType::from_str("seedream_4").unwrap(), ModelType::Seedream4);
assert_eq!(ModelType::from_str("seedream_4p5").unwrap(), ModelType::Seedream4p5);
assert_eq!(ModelType::from_str("midjourney").unwrap(), ModelType::Midjourney);
assert_eq!(ModelType::from_str("midjourney_v6").unwrap(), ModelType::MidjourneyV6);
assert_eq!(ModelType::from_str("midjourney_v6p1").unwrap(), ModelType::MidjourneyV6p1);
@@ -385,7 +409,7 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = ModelType::all_variants();
assert_eq!(variants.len(), 35);
assert_eq!(variants.len(), 38);
// Image models
assert_eq!(variants.pop_first(), Some(ModelType::Flux1Dev));
assert_eq!(variants.pop_first(), Some(ModelType::Flux1Schnell));
@@ -395,6 +419,7 @@ mod tests {
assert_eq!(variants.pop_first(), Some(ModelType::FluxPro11Ultra));
assert_eq!(variants.pop_first(), Some(ModelType::FluxProKontextMax));
assert_eq!(variants.pop_first(), Some(ModelType::GptImage1));
assert_eq!(variants.pop_first(), Some(ModelType::GptImage1p5));
assert_eq!(variants.pop_first(), Some(ModelType::GrokImage));
assert_eq!(variants.pop_first(), Some(ModelType::Recraft3));
assert_eq!(variants.pop_first(), Some(ModelType::SeedEdit3));
@@ -402,6 +427,8 @@ mod tests {
assert_eq!(variants.pop_first(), Some(ModelType::Gemini25Flash));
assert_eq!(variants.pop_first(), Some(ModelType::NanoBanana));
assert_eq!(variants.pop_first(), Some(ModelType::NanoBananaPro));
assert_eq!(variants.pop_first(), Some(ModelType::Seedream4));
assert_eq!(variants.pop_first(), Some(ModelType::Seedream4p5));
assert_eq!(variants.pop_first(), Some(ModelType::Midjourney));
assert_eq!(variants.pop_first(), Some(ModelType::MidjourneyV6));
assert_eq!(variants.pop_first(), Some(ModelType::MidjourneyV6p1));
@@ -35,6 +35,12 @@ pub enum TaskModelType {
NanoBananaPro,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "gpt_image_1p5")]
GptImage1p5,
#[serde(rename = "seedream_4")]
Seedream4,
#[serde(rename = "seedream_4p5")]
Seedream4p5,
#[serde(rename = "grok_image")]
GrokImage,
#[serde(rename = "recraft_3")]
@@ -94,6 +100,9 @@ impl TaskModelType {
Self::NanoBanana => "nano_banana",
Self::NanoBananaPro => "nano_banana_pro",
Self::GptImage1 => "gpt_image_1",
Self::GptImage1p5 => "gpt_image_1p5",
Self::Seedream4 => "seedream_4",
Self::Seedream4p5 => "seedream_4p5",
Self::GrokImage => "grok_image",
Self::Recraft3 => "recraft_3",
Self::Midjourney => "midjourney",
@@ -128,6 +137,9 @@ impl TaskModelType {
"nano_banana" => Ok(Self::NanoBanana),
"nano_banana_pro" => Ok(Self::NanoBananaPro),
"gpt_image_1" => Ok(Self::GptImage1),
"gpt_image_1p5" => Ok(Self::GptImage1p5),
"seedream_4" => Ok(Self::Seedream4),
"seedream_4p5" => Ok(Self::Seedream4p5),
"grok_image" => Ok(Self::GrokImage),
"recraft_3" => Ok(Self::Recraft3),
"midjourney" => Ok(Self::Midjourney),
@@ -165,6 +177,9 @@ impl TaskModelType {
Self::NanoBanana,
Self::NanoBananaPro,
Self::GptImage1,
Self::GptImage1p5,
Self::Seedream4,
Self::Seedream4p5,
Self::GrokImage,
Self::Recraft3,
Self::Midjourney,
@@ -209,6 +224,9 @@ mod tests {
assert_serialization(TaskModelType::NanoBanana, "nano_banana");
assert_serialization(TaskModelType::NanoBananaPro, "nano_banana_pro");
assert_serialization(TaskModelType::GptImage1, "gpt_image_1");
assert_serialization(TaskModelType::GptImage1p5, "gpt_image_1p5");
assert_serialization(TaskModelType::Seedream4, "seedream_4");
assert_serialization(TaskModelType::Seedream4p5, "seedream_4p5");
assert_serialization(TaskModelType::GrokImage, "grok_image");
assert_serialization(TaskModelType::Recraft3, "recraft_3");
assert_serialization(TaskModelType::Midjourney, "midjourney");
@@ -242,6 +260,9 @@ mod tests {
assert_eq!(TaskModelType::NanoBanana.to_str(), "nano_banana");
assert_eq!(TaskModelType::NanoBananaPro.to_str(), "nano_banana_pro");
assert_eq!(TaskModelType::GptImage1.to_str(), "gpt_image_1");
assert_eq!(TaskModelType::GptImage1p5.to_str(), "gpt_image_1p5");
assert_eq!(TaskModelType::Seedream4.to_str(), "seedream_4");
assert_eq!(TaskModelType::Seedream4p5.to_str(), "seedream_4p5");
assert_eq!(TaskModelType::GrokImage.to_str(), "grok_image");
assert_eq!(TaskModelType::Recraft3.to_str(), "recraft_3");
assert_eq!(TaskModelType::Midjourney.to_str(), "midjourney");
@@ -275,6 +296,9 @@ mod tests {
assert_eq!(TaskModelType::from_str("nano_banana").unwrap(), TaskModelType::NanoBanana);
assert_eq!(TaskModelType::from_str("nano_banana_pro").unwrap(), TaskModelType::NanoBananaPro);
assert_eq!(TaskModelType::from_str("gpt_image_1").unwrap(), TaskModelType::GptImage1);
assert_eq!(TaskModelType::from_str("gpt_image_1p5").unwrap(), TaskModelType::GptImage1p5);
assert_eq!(TaskModelType::from_str("seedream_4").unwrap(), TaskModelType::Seedream4);
assert_eq!(TaskModelType::from_str("seedream_4p5").unwrap(), TaskModelType::Seedream4p5);
assert_eq!(TaskModelType::from_str("grok_image").unwrap(), TaskModelType::GrokImage);
assert_eq!(TaskModelType::from_str("recraft_3").unwrap(), TaskModelType::Recraft3);
assert_eq!(TaskModelType::from_str("midjourney").unwrap(), TaskModelType::Midjourney);
@@ -308,7 +332,7 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = TaskModelType::all_variants();
assert_eq!(variants.len(), 26);
assert_eq!(variants.len(), 29);
// Image models
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Dev));
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Schnell));
@@ -321,6 +345,9 @@ mod tests {
assert_eq!(variants.pop_first(), Some(TaskModelType::NanoBanana));
assert_eq!(variants.pop_first(), Some(TaskModelType::NanoBananaPro));
assert_eq!(variants.pop_first(), Some(TaskModelType::GptImage1));
assert_eq!(variants.pop_first(), Some(TaskModelType::GptImage1p5));
assert_eq!(variants.pop_first(), Some(TaskModelType::Seedream4));
assert_eq!(variants.pop_first(), Some(TaskModelType::Seedream4p5));
assert_eq!(variants.pop_first(), Some(TaskModelType::GrokImage));
assert_eq!(variants.pop_first(), Some(TaskModelType::Recraft3));
assert_eq!(variants.pop_first(), Some(TaskModelType::Midjourney));
@@ -0,0 +1,301 @@
use std::collections::HashSet;
use std::iter::FromIterator;
use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
use crate::util::lookup::lookup_image_urls_as_optional_list::lookup_image_urls_as_optional_list;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::{BytedanceSeedreamV4MultiFunctionImageGenImageSize, BytedanceSeedreamV4MultiFunctionImageGenNumImages, BytedanceSeedreamV4MultiFunctionImageGenRequest, BytedanceSeedreamV4MultiFunctionImageGenResponse};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::{enqueue_bytedance_seedream_v4_edit_image_webhook, EnqueueBytedanceSeedreamV4EditImageArgs, EnqueueBytedanceSeedreamV4EditImageNumImages, EnqueueBytedanceSeedreamV4EditImageSize};
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{enqueue_bytedance_seedream_v4_text_to_image_webhook, EnqueueBytedanceSeedreamV4TextToImageArgs, EnqueueBytedanceSeedreamV4TextToImageNumImages, EnqueueBytedanceSeedreamV4TextToImageSize};
use http_server_common::request::get_request_ip::get_request_ip;
use log::{error, info, warn};
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use server_environment::ServerEnvironment;
use sqlx::pool::PoolConnection;
use sqlx::{Acquire, MySql};
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
/// Bytedance Seedream 4 Multi-Function (generate + edit)
#[utoipa::path(
post,
tag = "Generate Images (Multi-Function)",
path = "/v1/generate/image/multi_function/bytedance_seedream_v4",
responses(
(status = 200, description = "Success", body = BytedanceSeedreamV4MultiFunctionImageGenResponse),
),
params(
("request" = BytedanceSeedreamV4MultiFunctionImageGenRequest, description = "Payload for Request"),
)
)]
pub async fn bytedance_seedream_v4_multi_function_image_gen_handler(
http_request: HttpRequest,
request: Json<BytedanceSeedreamV4MultiFunctionImageGenRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<BytedanceSeedreamV4MultiFunctionImageGenResponse>, CommonWebError> {
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
// Some(session) => session,
// None => {
// warn!("not logged in");
// return Err(CommonWebError::NotAuthorized);
// }
//};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
let image_urls = match request.image_media_tokens.as_ref() {
Some(media_tokens) => {
info!("Looking up image media tokens: {:?}", media_tokens);
lookup_image_urls_as_optional_list(
&http_request,
&mut mysql_connection,
server_state.server_environment,
media_tokens,
).await?
},
None => None,
};
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
})?;
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let fal_result;
if let Some(input_image_urls) = image_urls.as_deref() {
info!("edit image case");
let num_images = match request.num_images {
Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::One) => EnqueueBytedanceSeedreamV4EditImageNumImages::One,
Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Two) => EnqueueBytedanceSeedreamV4EditImageNumImages::Two,
Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Three) => EnqueueBytedanceSeedreamV4EditImageNumImages::Three,
Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Four) => EnqueueBytedanceSeedreamV4EditImageNumImages::Four,
None => EnqueueBytedanceSeedreamV4EditImageNumImages::One, // Default to One
};
let image_size = match request.image_size {
// Square
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::Square) => EnqueueBytedanceSeedreamV4EditImageSize::Square,
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::SquareHd) => EnqueueBytedanceSeedreamV4EditImageSize::SquareHd,
// Tall
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::PortraitFourThree) => EnqueueBytedanceSeedreamV4EditImageSize::PortraitFourThree,
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::PortraitSixteenNine) => EnqueueBytedanceSeedreamV4EditImageSize::PortraitSixteenNine,
// Wide
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::LandscapeFourThree) => EnqueueBytedanceSeedreamV4EditImageSize::LandscapeFourThree,
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::LandscapeSixteenNine) => EnqueueBytedanceSeedreamV4EditImageSize::LandscapeSixteenNine,
// Auto
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::Auto) => EnqueueBytedanceSeedreamV4EditImageSize::Auto,
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::Auto2k) => EnqueueBytedanceSeedreamV4EditImageSize::Auto2k,
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::Auto4k) => EnqueueBytedanceSeedreamV4EditImageSize::Auto4k,
None => EnqueueBytedanceSeedreamV4EditImageSize::SquareHd,
};
let args = EnqueueBytedanceSeedreamV4EditImageArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
image_urls: input_image_urls.to_owned(),
num_images: Some(num_images),
image_size: Some(image_size),
max_images: None,
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
fal_result = enqueue_bytedance_seedream_v4_edit_image_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_bytedance_seedream_v4_edit_image_webhook: {:?}", err);
CommonWebError::ServerError
})?;
} else {
info!("text-to-image case");
let num_images = match request.num_images {
Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::One) => EnqueueBytedanceSeedreamV4TextToImageNumImages::One,
Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Two) => EnqueueBytedanceSeedreamV4TextToImageNumImages::Two,
Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Three) => EnqueueBytedanceSeedreamV4TextToImageNumImages::Three,
Some(BytedanceSeedreamV4MultiFunctionImageGenNumImages::Four) => EnqueueBytedanceSeedreamV4TextToImageNumImages::Four,
None => EnqueueBytedanceSeedreamV4TextToImageNumImages::One, // Default to One
};
let image_size = match request.image_size {
// Square
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::Square) => EnqueueBytedanceSeedreamV4TextToImageSize::Square,
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::SquareHd) => EnqueueBytedanceSeedreamV4TextToImageSize::SquareHd,
// Tall
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::PortraitFourThree) => EnqueueBytedanceSeedreamV4TextToImageSize::PortraitFourThree,
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::PortraitSixteenNine) => EnqueueBytedanceSeedreamV4TextToImageSize::PortraitSixteenNine,
// Wide
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::LandscapeFourThree) => EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeFourThree,
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::LandscapeSixteenNine) => EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeSixteenNine,
// Auto
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::Auto) => EnqueueBytedanceSeedreamV4TextToImageSize::Auto,
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::Auto2k) => EnqueueBytedanceSeedreamV4TextToImageSize::Auto2k,
Some(BytedanceSeedreamV4MultiFunctionImageGenImageSize::Auto4k) => EnqueueBytedanceSeedreamV4TextToImageSize::Auto4k,
None => EnqueueBytedanceSeedreamV4TextToImageSize::SquareHd,
};
let args = EnqueueBytedanceSeedreamV4TextToImageArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
num_images: Some(num_images),
image_size: Some(image_size),
max_images: None,
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
fal_result = enqueue_bytedance_seedream_v4_text_to_image_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_bytedance_seedream_v4_text_to_image_webhook: {:?}", err);
CommonWebError::ServerError
})?;
}
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
CommonWebError::ServerError
})?;
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::Seedream4),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
if let Some(media_tokens) = &request.image_media_tokens {
if let Some(token) = prompt_token.as_ref() {
let result = insert_batch_prompt_context_items(InsertBatchArgs {
prompt_token: token.clone(),
items: media_tokens.iter().map(|token| {
PromptContextItem {
media_token: token.clone(),
context_semantic_type: PromptContextSemanticType::Imgref,
}
}).collect(),
transaction: &mut transaction,
}).await;
if let Err(err) = result {
// NB: Fail open.
warn!("Error inserting batch prompt context items: {:?}", err);
}
}
}
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(BytedanceSeedreamV4MultiFunctionImageGenResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -0,0 +1,299 @@
use std::collections::HashSet;
use std::iter::FromIterator;
use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
use crate::util::lookup::lookup_image_urls_as_optional_list::lookup_image_urls_as_optional_list;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::{BytedanceSeedreamV4p5MultiFunctionImageGenImageSize, BytedanceSeedreamV4p5MultiFunctionImageGenNumImages, BytedanceSeedreamV4p5MultiFunctionImageGenRequest, BytedanceSeedreamV4p5MultiFunctionImageGenResponse};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{enqueue_bytedance_seedream_v4p5_edit_image_webhook, EnqueueBytedanceSeedreamV4p5EditImageArgs, EnqueueBytedanceSeedreamV4p5EditImageNumImages, EnqueueBytedanceSeedreamV4p5EditImageSize};
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{enqueue_bytedance_seedream_v4p5_text_to_image_webhook, EnqueueBytedanceSeedreamV4p5TextToImageArgs, EnqueueBytedanceSeedreamV4p5TextToImageNumImages, EnqueueBytedanceSeedreamV4p5TextToImageSize};
use http_server_common::request::get_request_ip::get_request_ip;
use log::{error, info, warn};
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use server_environment::ServerEnvironment;
use sqlx::pool::PoolConnection;
use sqlx::{Acquire, MySql};
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
/// Bytedance Seedream 4.5 Multi-Function (generate + edit)
#[utoipa::path(
post,
tag = "Generate Images (Multi-Function)",
path = "/v1/generate/image/multi_function/bytedance_seedream_v4p5",
responses(
(status = 200, description = "Success", body = BytedanceSeedreamV4p5MultiFunctionImageGenResponse),
),
params(
("request" = BytedanceSeedreamV4p5MultiFunctionImageGenRequest, description = "Payload for Request"),
)
)]
pub async fn bytedance_seedream_v4p5_multi_function_image_gen_handler(
http_request: HttpRequest,
request: Json<BytedanceSeedreamV4p5MultiFunctionImageGenRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<BytedanceSeedreamV4p5MultiFunctionImageGenResponse>, CommonWebError> {
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
// Some(session) => session,
// None => {
// warn!("not logged in");
// return Err(CommonWebError::NotAuthorized);
// }
//};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
let image_urls = match request.image_media_tokens.as_ref() {
Some(media_tokens) => {
info!("Looking up image media tokens: {:?}", media_tokens);
lookup_image_urls_as_optional_list(
&http_request,
&mut mysql_connection,
server_state.server_environment,
media_tokens,
).await?
},
None => None,
};
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
})?;
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let fal_result;
if let Some(input_image_urls) = image_urls.as_deref() {
info!("edit image case");
let num_images = match request.num_images {
Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::One) => EnqueueBytedanceSeedreamV4p5EditImageNumImages::One,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Two) => EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Three) => EnqueueBytedanceSeedreamV4p5EditImageNumImages::Three,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Four) => EnqueueBytedanceSeedreamV4p5EditImageNumImages::Four,
None => EnqueueBytedanceSeedreamV4p5EditImageNumImages::One, // Default to One
};
let image_size = match request.image_size {
// Square
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::Square) => EnqueueBytedanceSeedreamV4p5EditImageSize::Square,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::SquareHd) => EnqueueBytedanceSeedreamV4p5EditImageSize::SquareHd,
// Tall
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::PortraitFourThree) => EnqueueBytedanceSeedreamV4p5EditImageSize::PortraitFourThree,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::PortraitSixteenNine) => EnqueueBytedanceSeedreamV4p5EditImageSize::PortraitSixteenNine,
// Wide
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::LandscapeFourThree) => EnqueueBytedanceSeedreamV4p5EditImageSize::LandscapeFourThree,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::LandscapeSixteenNine) => EnqueueBytedanceSeedreamV4p5EditImageSize::LandscapeSixteenNine,
// Auto
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::Auto2k) => EnqueueBytedanceSeedreamV4p5EditImageSize::Auto2k,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::Auto4k) => EnqueueBytedanceSeedreamV4p5EditImageSize::Auto4k,
None => EnqueueBytedanceSeedreamV4p5EditImageSize::SquareHd,
};
let args = EnqueueBytedanceSeedreamV4p5EditImageArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
image_urls: input_image_urls.to_owned(),
num_images: Some(num_images),
image_size: Some(image_size),
max_images: None,
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
fal_result = enqueue_bytedance_seedream_v4p5_edit_image_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_bytedance_seedream_v4p5_edit_image_webhook: {:?}", err);
CommonWebError::ServerError
})?;
} else {
info!("text-to-image case");
let num_images = match request.num_images {
Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::One) => EnqueueBytedanceSeedreamV4p5TextToImageNumImages::One,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Two) => EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Three) => EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Three,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Four) => EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Four,
None => EnqueueBytedanceSeedreamV4p5TextToImageNumImages::One, // Default to One
};
let image_size = match request.image_size {
// Square
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::Square) => EnqueueBytedanceSeedreamV4p5TextToImageSize::Square,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::SquareHd) => EnqueueBytedanceSeedreamV4p5TextToImageSize::SquareHd,
// Tall
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::PortraitFourThree) => EnqueueBytedanceSeedreamV4p5TextToImageSize::PortraitFourThree,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::PortraitSixteenNine) => EnqueueBytedanceSeedreamV4p5TextToImageSize::PortraitSixteenNine,
// Wide
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::LandscapeFourThree) => EnqueueBytedanceSeedreamV4p5TextToImageSize::LandscapeFourThree,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::LandscapeSixteenNine) => EnqueueBytedanceSeedreamV4p5TextToImageSize::LandscapeSixteenNine,
// Auto
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::Auto2k) => EnqueueBytedanceSeedreamV4p5TextToImageSize::Auto2k,
Some(BytedanceSeedreamV4p5MultiFunctionImageGenImageSize::Auto4k) => EnqueueBytedanceSeedreamV4p5TextToImageSize::Auto4k,
None => EnqueueBytedanceSeedreamV4p5TextToImageSize::SquareHd,
};
let args = EnqueueBytedanceSeedreamV4p5TextToImageArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
num_images: Some(num_images),
image_size: Some(image_size),
max_images: None,
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
fal_result = enqueue_bytedance_seedream_v4p5_text_to_image_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_bytedance_seedream_v4p5_text_to_image_webhook: {:?}", err);
CommonWebError::ServerError
})?;
}
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
CommonWebError::ServerError
})?;
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::Seedream4p5),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
if let Some(media_tokens) = &request.image_media_tokens {
if let Some(token) = prompt_token.as_ref() {
let result = insert_batch_prompt_context_items(InsertBatchArgs {
prompt_token: token.clone(),
items: media_tokens.iter().map(|token| {
PromptContextItem {
media_token: token.clone(),
context_semantic_type: PromptContextSemanticType::Imgref,
}
}).collect(),
transaction: &mut transaction,
}).await;
if let Err(err) = result {
// NB: Fail open.
warn!("Error inserting batch prompt context items: {:?}", err);
}
}
}
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(BytedanceSeedreamV4p5MultiFunctionImageGenResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -0,0 +1,355 @@
use std::collections::{HashMap, HashSet};
use std::iter::FromIterator;
use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
use crate::util::lookup::lookup_image_urls_as_map::lookup_image_urls_as_map;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{GptImage1p5MultiFunctionImageGenBackground, GptImage1p5MultiFunctionImageGenInputFidelity, GptImage1p5MultiFunctionImageGenNumImages, GptImage1p5MultiFunctionImageGenQuality, GptImage1p5MultiFunctionImageGenRequest, GptImage1p5MultiFunctionImageGenResponse, GptImage1p5MultiFunctionImageGenSize};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1p5_edit_image_webhook::{enqueue_gpt_image_1p5_image_edit_webhook, EnqueueGptImage1p5EditImageArgs, EnqueueGptImage1p5EditImageBackground, EnqueueGptImage1p5EditImageInputFidelity, EnqueueGptImage1p5EditImageNumImages, EnqueueGptImage1p5EditImageQuality, EnqueueGptImage1p5EditImageSize};
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1p5_text_to_image_webhook::{enqueue_gpt_image_1p5_text_to_image_webhook, EnqueueGptImage1p5TextToImageArgs, EnqueueGptImage1p5TextToImageBackground, EnqueueGptImage1p5TextToImageNumImages, EnqueueGptImage1p5TextToImageQuality, EnqueueGptImage1p5TextToImageSize};
use http_server_common::request::get_request_ip::get_request_ip;
use log::{error, info, warn};
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use server_environment::ServerEnvironment;
use sqlx::pool::PoolConnection;
use sqlx::{Acquire, MySql};
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
/// Gpt Image 1.5 Multi-Function (generate + edit + inpaint)
#[utoipa::path(
post,
tag = "Generate Images (Multi-Function)",
path = "/v1/generate/image/multi_function/gpt_image_1p5",
responses(
(status = 200, description = "Success", body = GptImage1p5MultiFunctionImageGenResponse),
),
params(
("request" = GptImage1p5MultiFunctionImageGenRequest, description = "Payload for Request"),
)
)]
pub async fn gpt_image_1p5_multi_function_image_gen_handler(
http_request: HttpRequest,
request: Json<GptImage1p5MultiFunctionImageGenRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GptImage1p5MultiFunctionImageGenResponse>, CommonWebError> {
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
// Some(session) => session,
// None => {
// warn!("not logged in");
// return Err(CommonWebError::NotAuthorized);
// }
//};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
let mut query_media_tokens = None;
if let Some(media_tokens) = request.image_media_tokens.as_ref() {
if !media_tokens.is_empty() {
let mut tokens = Vec::new();
tokens.extend(media_tokens.iter().map(|token| token.to_owned()));
if let Some(mask_media_token) = request.mask_image_token.as_ref() {
// NB: Masks are only relevant when this is an image editing exercise.
tokens.push(mask_media_token.to_owned());
}
query_media_tokens = Some(tokens);
}
}
let image_urls_by_token = match query_media_tokens {
None => HashMap::new(),
Some(media_tokens) => {
info!("Looking up image media tokens: {:?}", media_tokens);
lookup_image_urls_as_map(
&http_request,
&mut mysql_connection,
server_state.server_environment,
&media_tokens,
).await?
}
};
let maybe_image_urls = match request.image_media_tokens.as_ref() {
None => None,
Some(tokens) => {
let urls = tokens.iter()
.filter_map(|token| image_urls_by_token.get(token))
.map(|url| url.to_string())
.collect::<Vec<_>>();
Some(urls)
}
};
let maybe_mask_url = match request.mask_image_token.as_ref() {
None => None,
Some(token) => {
image_urls_by_token.get(&token).map(|url| url.to_string())
}
};
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
})?;
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let fal_result;
if let Some(input_image_urls) = maybe_image_urls.as_deref() {
info!("gpt image 1.5 edit image");
let num_images = match request.num_images {
Some(GptImage1p5MultiFunctionImageGenNumImages::One) => EnqueueGptImage1p5EditImageNumImages::One,
Some(GptImage1p5MultiFunctionImageGenNumImages::Two) => EnqueueGptImage1p5EditImageNumImages::Two,
Some(GptImage1p5MultiFunctionImageGenNumImages::Three) => EnqueueGptImage1p5EditImageNumImages::Three,
Some(GptImage1p5MultiFunctionImageGenNumImages::Four) => EnqueueGptImage1p5EditImageNumImages::Four,
None => EnqueueGptImage1p5EditImageNumImages::One, // Default to One
};
let image_size = match request.image_size {
Some(GptImage1p5MultiFunctionImageGenSize::Square) => EnqueueGptImage1p5EditImageSize::Square,
Some(GptImage1p5MultiFunctionImageGenSize::Wide) => EnqueueGptImage1p5EditImageSize::Wide,
Some(GptImage1p5MultiFunctionImageGenSize::Tall) => EnqueueGptImage1p5EditImageSize::Tall,
None => EnqueueGptImage1p5EditImageSize::Square,
};
let background = match request.background {
Some(GptImage1p5MultiFunctionImageGenBackground::Auto) => EnqueueGptImage1p5EditImageBackground::Auto,
Some(GptImage1p5MultiFunctionImageGenBackground::Transparent) => EnqueueGptImage1p5EditImageBackground::Transparent,
Some(GptImage1p5MultiFunctionImageGenBackground::Opaque) => EnqueueGptImage1p5EditImageBackground::Opaque,
None => EnqueueGptImage1p5EditImageBackground::Auto,
};
let quality = match request.quality {
Some(GptImage1p5MultiFunctionImageGenQuality::Low) => EnqueueGptImage1p5EditImageQuality::Low,
Some(GptImage1p5MultiFunctionImageGenQuality::Medium) => EnqueueGptImage1p5EditImageQuality::Medium,
Some(GptImage1p5MultiFunctionImageGenQuality::High) => EnqueueGptImage1p5EditImageQuality::High,
None => EnqueueGptImage1p5EditImageQuality::High,
};
let input_fidelity = match request.input_fidelity {
Some(GptImage1p5MultiFunctionImageGenInputFidelity::Low) => EnqueueGptImage1p5EditImageInputFidelity::Low,
Some(GptImage1p5MultiFunctionImageGenInputFidelity::High) => EnqueueGptImage1p5EditImageInputFidelity::High,
None => EnqueueGptImage1p5EditImageInputFidelity::High,
};
let args = EnqueueGptImage1p5EditImageArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
image_urls: input_image_urls.to_owned(),
num_images,
mask_image_url: maybe_mask_url,
image_size: Some(image_size),
background: Some(background),
quality: Some(quality),
input_fidelity: Some(input_fidelity),
output_format: None,
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
fal_result = enqueue_gpt_image_1p5_image_edit_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_gpt_image_1p5_image_edit_webhook: {:?}", err);
CommonWebError::ServerError
})?;
} else {
info!("gpt image 1.5 text-to-image");
let num_images = match request.num_images {
Some(GptImage1p5MultiFunctionImageGenNumImages::One) => EnqueueGptImage1p5TextToImageNumImages::One,
Some(GptImage1p5MultiFunctionImageGenNumImages::Two) => EnqueueGptImage1p5TextToImageNumImages::Two,
Some(GptImage1p5MultiFunctionImageGenNumImages::Three) => EnqueueGptImage1p5TextToImageNumImages::Three,
Some(GptImage1p5MultiFunctionImageGenNumImages::Four) => EnqueueGptImage1p5TextToImageNumImages::Four,
None => EnqueueGptImage1p5TextToImageNumImages::One, // Default to One
};
let image_size = match request.image_size {
Some(GptImage1p5MultiFunctionImageGenSize::Square) => EnqueueGptImage1p5TextToImageSize::Square,
Some(GptImage1p5MultiFunctionImageGenSize::Wide) => EnqueueGptImage1p5TextToImageSize::Wide,
Some(GptImage1p5MultiFunctionImageGenSize::Tall) => EnqueueGptImage1p5TextToImageSize::Tall,
None => EnqueueGptImage1p5TextToImageSize::Square,
};
let background = match request.background {
Some(GptImage1p5MultiFunctionImageGenBackground::Auto) => EnqueueGptImage1p5TextToImageBackground::Auto,
Some(GptImage1p5MultiFunctionImageGenBackground::Transparent) => EnqueueGptImage1p5TextToImageBackground::Transparent,
Some(GptImage1p5MultiFunctionImageGenBackground::Opaque) => EnqueueGptImage1p5TextToImageBackground::Opaque,
None => EnqueueGptImage1p5TextToImageBackground::Auto,
};
let quality = match request.quality {
Some(GptImage1p5MultiFunctionImageGenQuality::Low) => EnqueueGptImage1p5TextToImageQuality::Low,
Some(GptImage1p5MultiFunctionImageGenQuality::Medium) => EnqueueGptImage1p5TextToImageQuality::Medium,
Some(GptImage1p5MultiFunctionImageGenQuality::High) => EnqueueGptImage1p5TextToImageQuality::High,
None => EnqueueGptImage1p5TextToImageQuality::High,
};
let args = EnqueueGptImage1p5TextToImageArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
num_images,
output_format: None,
image_size: Some(image_size),
background: Some(background),
quality: Some(quality),
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
fal_result = enqueue_gpt_image_1p5_text_to_image_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_gpt_image_1p5_text_to_image_webhook: {:?}", err);
CommonWebError::ServerError
})?;
}
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
CommonWebError::ServerError
})?;
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the query fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::GptImage1p5),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None // Don't fail the job if the prompt insertion fails.
}
};
if let Some(media_tokens) = &request.image_media_tokens {
if let Some(token) = prompt_token.as_ref() {
let result = insert_batch_prompt_context_items(InsertBatchArgs {
prompt_token: token.clone(),
items: media_tokens.iter().map(|token| {
PromptContextItem {
media_token: token.clone(),
context_semantic_type: PromptContextSemanticType::Imgref,
}
}).collect(),
transaction: &mut transaction,
}).await;
if let Err(err) = result {
// NB: Fail open.
warn!("Error inserting batch prompt context items: {:?}", err);
}
}
}
let db_result = insert_generic_inference_job_for_fal_queue(InsertGenericInferenceForFalArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
maybe_external_third_party_id: &external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let job_token = match db_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GptImage1p5MultiFunctionImageGenResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -1 +1,4 @@
pub mod bytedance_seedream_v4_multi_function_image_gen_handler;
pub mod bytedance_seedream_v4p5_multi_function_image_gen_handler;
pub mod gpt_image_1p5_multi_function_image_gen_handler;
pub mod nano_banana_pro_multi_function_image_gen_handler;
@@ -8,6 +8,7 @@ use crate::http_server::endpoints::generate::common::payments_error_test::paymen
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
use crate::util::lookup::lookup_image_urls_as_optional_list::lookup_image_urls_as_optional_list;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{NanoBananaProMultiFunctionImageGenAspectRatio, NanoBananaProMultiFunctionImageGenImageResolution, NanoBananaProMultiFunctionImageGenNumImages, NanoBananaProMultiFunctionImageGenRequest, NanoBananaProMultiFunctionImageGenResponse};
@@ -89,7 +90,7 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
let image_urls = match request.image_media_tokens.as_ref() {
Some(media_tokens) => {
info!("Looking up image media tokens: {:?}", media_tokens);
lookup_image_urls(
lookup_image_urls_as_optional_list(
&http_request,
&mut mysql_connection,
server_state.server_environment,
@@ -251,7 +252,6 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
}
};
if let Some(media_tokens) = &request.image_media_tokens {
if let Some(token) = prompt_token.as_ref() {
let result = insert_batch_prompt_context_items(InsertBatchArgs {
@@ -307,66 +307,3 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
inference_job_token: job_token,
}))
}
async fn lookup_image_urls(
http_request: &HttpRequest,
mysql_connection: &mut PoolConnection<MySql>,
server_environment: ServerEnvironment,
tokens: &[MediaFileToken],
) -> Result<Option<Vec<String>>, CommonWebError> {
const CAN_SEE_DELETED: bool = false;
let result = batch_get_media_files_by_tokens_with_connection(
mysql_connection,
tokens,
CAN_SEE_DELETED,
).await;
let media_files = match result {
Ok(files) => files,
Err(err) => {
error!("Error getting media files by tokens: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
if media_files.len() != tokens.len() {
warn!("Wrong number of media files returned for tokens: {} found for {} tokens", media_files.len(), tokens.len());
let requested : HashSet<&MediaFileToken> = HashSet::from_iter(tokens.iter());
let returned : HashSet<&MediaFileToken> = HashSet::from_iter(media_files.iter().map(|m| &m.token));
let diff = requested.difference(&returned)
.cloned()
.collect::<Vec<&MediaFileToken>>();
return Err(CommonWebError::BadInputWithSimpleMessage(
format!("Not all media files could be found. Media files found: {}, tokens provided: {}, in original: {:?}, req {:?}, ret {:?}",
media_files.len(), tokens.len(), diff, requested, returned)));
}
let media_domain = get_media_domain(&http_request);
let image_urls = media_files.iter()
.map(|file| {
let public_bucket_path = MediaFileBucketPath::from_object_hash(
&file.public_bucket_directory_hash,
file.maybe_public_bucket_prefix.as_deref(),
file.maybe_public_bucket_extension.as_deref());
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_environment,
&public_bucket_path);
media_links.cdn_url.to_string()
})
.collect::<Vec<_>>();
if image_urls.is_empty() {
return Ok(None)
}
Ok(Some(image_urls))
}
@@ -10,6 +10,9 @@ use crate::http_server::endpoints::generate::image::generate_flux_pro_11_ultra_t
use crate::http_server::endpoints::generate::image::generate_gpt_image_1_text_to_image_handler::generate_gpt_image_1_text_to_image_handler;
use crate::http_server::endpoints::generate::image::inpaint::flux_dev_juggernaut_inpaint_handler::flux_dev_juggernaut_inpaint_image_handler;
use crate::http_server::endpoints::generate::image::inpaint::flux_pro_1_inpaint_handler::flux_pro_1_inpaint_image_handler;
use crate::http_server::endpoints::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen_handler::bytedance_seedream_v4_multi_function_image_gen_handler;
use crate::http_server::endpoints::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen_handler::bytedance_seedream_v4p5_multi_function_image_gen_handler;
use crate::http_server::endpoints::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen_handler::gpt_image_1p5_multi_function_image_gen_handler;
use crate::http_server::endpoints::generate::image::multi_function::nano_banana_pro_multi_function_image_gen_handler::nano_banana_pro_multi_function_image_gen_handler;
use crate::http_server::endpoints::generate::image::remove_image_background_handler::remove_image_background_handler;
use crate::http_server::endpoints::generate::object::generate_hunyuan_2_0_image_to_3d_handler::generate_hunyuan_2_0_image_to_3d_handler;
@@ -41,6 +44,18 @@ where
app.service(web::scope("/v1/generate")
.service(web::scope("/image")
.service(web::scope("/multi_function")
.service(web::resource("/bytedance_seedream_4")
.route(web::post().to(bytedance_seedream_v4_multi_function_image_gen_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/bytedance_seedream_4p5")
.route(web::post().to(bytedance_seedream_v4p5_multi_function_image_gen_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/gpt_image_1p5")
.route(web::post().to(gpt_image_1p5_multi_function_image_gen_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/nano_banana_pro")
.route(web::post().to(nano_banana_pro_multi_function_image_gen_handler))
.route(web::head().to(|| HttpResponse::Ok()))
@@ -0,0 +1,71 @@
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use actix_web::HttpRequest;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use log::{error, warn};
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::batch_get_media_files_by_tokens_with_connection;
use server_environment::ServerEnvironment;
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use std::collections::{HashMap, HashSet};
use std::iter::FromIterator;
use tokens::tokens::media_files::MediaFileToken;
pub async fn lookup_image_urls_as_map(
http_request: &HttpRequest,
mysql_connection: &mut PoolConnection<MySql>,
server_environment: ServerEnvironment,
tokens: &[MediaFileToken],
) -> Result<HashMap<MediaFileToken, String>, CommonWebError> {
const CAN_SEE_DELETED: bool = false;
let result = batch_get_media_files_by_tokens_with_connection(
mysql_connection,
tokens,
CAN_SEE_DELETED,
).await;
let media_files = match result {
Ok(files) => files,
Err(err) => {
error!("Error getting media files by tokens: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
if media_files.len() != tokens.len() {
warn!("Wrong number of media files returned for tokens: {} found for {} tokens", media_files.len(), tokens.len());
let requested : HashSet<&MediaFileToken> = HashSet::from_iter(tokens.iter());
let returned : HashSet<&MediaFileToken> = HashSet::from_iter(media_files.iter().map(|m| &m.token));
let diff = requested.difference(&returned)
.cloned()
.collect::<Vec<&MediaFileToken>>();
return Err(CommonWebError::BadInputWithSimpleMessage(
format!("Not all media files could be found. Media files found: {}, tokens provided: {}, in original: {:?}, req {:?}, ret {:?}",
media_files.len(), tokens.len(), diff, requested, returned)));
}
let media_domain = get_media_domain(&http_request);
let image_urls = media_files.into_iter()
.map(|file| {
let public_bucket_path = MediaFileBucketPath::from_object_hash(
&file.public_bucket_directory_hash,
file.maybe_public_bucket_prefix.as_deref(),
file.maybe_public_bucket_extension.as_deref());
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_environment,
&public_bucket_path);
(file.token, media_links.cdn_url.to_string())
})
.collect::<HashMap<_, _>>();
Ok(image_urls)
}
@@ -0,0 +1,75 @@
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use actix_web::HttpRequest;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use log::{error, warn};
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::batch_get_media_files_by_tokens_with_connection;
use server_environment::ServerEnvironment;
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use std::collections::HashSet;
use std::iter::FromIterator;
use tokens::tokens::media_files::MediaFileToken;
pub async fn lookup_image_urls_as_optional_list(
http_request: &HttpRequest,
mysql_connection: &mut PoolConnection<MySql>,
server_environment: ServerEnvironment,
tokens: &[MediaFileToken],
) -> Result<Option<Vec<String>>, CommonWebError> {
const CAN_SEE_DELETED: bool = false;
let result = batch_get_media_files_by_tokens_with_connection(
mysql_connection,
tokens,
CAN_SEE_DELETED,
).await;
let media_files = match result {
Ok(files) => files,
Err(err) => {
error!("Error getting media files by tokens: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
if media_files.len() != tokens.len() {
warn!("Wrong number of media files returned for tokens: {} found for {} tokens", media_files.len(), tokens.len());
let requested : HashSet<&MediaFileToken> = HashSet::from_iter(tokens.iter());
let returned : HashSet<&MediaFileToken> = HashSet::from_iter(media_files.iter().map(|m| &m.token));
let diff = requested.difference(&returned)
.cloned()
.collect::<Vec<&MediaFileToken>>();
return Err(CommonWebError::BadInputWithSimpleMessage(
format!("Not all media files could be found. Media files found: {}, tokens provided: {}, in original: {:?}, req {:?}, ret {:?}",
media_files.len(), tokens.len(), diff, requested, returned)));
}
let media_domain = get_media_domain(&http_request);
let image_urls = media_files.iter()
.map(|file| {
let public_bucket_path = MediaFileBucketPath::from_object_hash(
&file.public_bucket_directory_hash,
file.maybe_public_bucket_prefix.as_deref(),
file.maybe_public_bucket_extension.as_deref());
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_environment,
&public_bucket_path);
media_links.cdn_url.to_string()
})
.collect::<Vec<_>>();
if image_urls.is_empty() {
return Ok(None)
}
Ok(Some(image_urls))
}
@@ -1 +1,3 @@
pub mod fetch_all_required_media_files;
pub mod fetch_all_required_media_files;
pub mod lookup_image_urls_as_optional_list;
pub mod lookup_image_urls_as_map;
@@ -0,0 +1,53 @@
use crate::prelude::{Deserialize, FalRequest, Serialize};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct GptImage1p5ImageEditInput {
pub prompt: String,
pub image_urls: Vec<String>,
/// The URL of the mask image to use for the generation. This indicates what part of the image to edit.
#[serde(skip_serializing_if = "Option::is_none")]
pub mask_image_url: Option<String>,
/// Eg. auto, 1024x1024, 1536x1024, 1024x1536
#[serde(skip_serializing_if = "Option::is_none")]
pub image_size: Option<String>,
/// Eg. Possible enum values: auto, transparent, opaque
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<String>,
/// Eg. Possible enum values: low, medium, high
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<String>,
/// Eg. Possible enum values: low, high
#[serde(skip_serializing_if = "Option::is_none")]
pub input_fidelity: Option<String>,
/// 1 - 4
#[serde(skip_serializing_if = "Option::is_none")]
pub num_images: Option<u8>,
/// Possible enum values: jpeg, png, webp
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage1p5ImageEditOutput {
pub images: Vec<ImageFile>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ImageFile {
/// The URL where the file can be downloaded from.
pub url: String,
}
pub fn gpt_image_1p5_image_edit(
params: GptImage1p5ImageEditInput,
) -> FalRequest<GptImage1p5ImageEditInput, GptImage1p5ImageEditOutput> {
FalRequest::new("fal-ai/gpt-image-1.5/edit", params)
}
@@ -0,0 +1,43 @@
use crate::prelude::{Deserialize, FalRequest, Serialize};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct GptImage1p5TextToImageInput {
pub prompt: String,
/// Eg. auto, 1024x1024, 1536x1024, 1024x1536
#[serde(skip_serializing_if = "Option::is_none")]
pub image_size: Option<String>,
/// Eg. Possible enum values: auto, transparent, opaque
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<String>,
/// Eg. Possible enum values: low, medium, high
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<String>,
/// 1 - 4
#[serde(skip_serializing_if = "Option::is_none")]
pub num_images: Option<u8>,
/// Possible enum values: jpeg, png, webp
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ImageFile {
/// The URL where the file can be downloaded from.
pub url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage1p5TextToImageOutput {
pub images: Vec<ImageFile>,
}
pub fn gpt_image_1p5_text_to_image(
params: GptImage1p5TextToImageInput,
) -> FalRequest<GptImage1p5TextToImageInput, GptImage1p5TextToImageOutput> {
FalRequest::new("fal-ai/gpt-image-1.5", params)
}
@@ -0,0 +1,2 @@
pub mod gpt_image_1p5_text_to_image;
pub mod gpt_image_1p5_image_edit;
+14
View File
@@ -935,6 +935,20 @@ pub mod got_ocr;
feature = "endpoints_fal-ai_gpt"
)))
)]
pub mod gpt_image;
#[cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_gpt"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "endpoints",
feature = "endpoints_fal-ai",
feature = "endpoints_fal-ai_gpt"
)))
)]
pub mod gpt_image_1;
#[cfg(any(
feature = "endpoints",