mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
omni gen for a few image models (#1308)
This commit is contained in:
@@ -51,6 +51,10 @@ then the story, then the fine print.
|
||||
- Group imports: std, external crates, internal workspace crates, `crate::` imports
|
||||
- Use fully qualified paths for one-off references; `use` for repeated references
|
||||
- Prefer specific imports over wildcards, except in `api_doc.rs` and test modules
|
||||
- When two crates export a type with the same name, alias with a suffix describing the source crate:
|
||||
`use enums::...::CommonResolution as CommonResolutionEnum;`
|
||||
`use artcraft_router::...::CommonResolution as CommonResolutionRouter;`
|
||||
This avoids long fully-qualified paths in function signatures and bodies.
|
||||
|
||||
## TypeScript / Frontend
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ artcraft/
|
||||
- SQLx for MySQL and SQLite
|
||||
- A mix of wreq and reqwest for Rust HTTP clients
|
||||
- Never use `println!` or `eprintln!` outside of tests; use `log` crate macros instead
|
||||
- When two crates export the same type name, alias imports with a suffix: `use foo::Bar as BarFoo;`
|
||||
- TypeScript with Nx, React, Vite, Zustand, and Three.js
|
||||
- Use two spaces for indentation
|
||||
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
$kind: http-request
|
||||
name: Add Wallet Banked Balance
|
||||
url: "{{api_host}}/v1/moderation/wallet/wallet_2pz2h3m2bdfvna7m3b92ccwwr/add_ba\
|
||||
nked_balance"
|
||||
method: POST
|
||||
headers:
|
||||
- key: Accept
|
||||
value: application/json
|
||||
- key: Session
|
||||
value: ""
|
||||
disabled: true
|
||||
- key: Session
|
||||
value: ""
|
||||
disabled: true
|
||||
body:
|
||||
type: json
|
||||
content: |
|
||||
{
|
||||
"title": "test alert from thread 2",
|
||||
"description": "testing testing test"
|
||||
}
|
||||
order: 3365370184335612
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
$kind: http-request
|
||||
name: Create Wallet for User
|
||||
url: "{{api_host}}/v1/moderation/wallet/create_for_user"
|
||||
method: POST
|
||||
headers:
|
||||
- key: Accept
|
||||
value: application/json
|
||||
- key: Session
|
||||
value: ""
|
||||
disabled: true
|
||||
- key: Session
|
||||
value: ""
|
||||
disabled: true
|
||||
body:
|
||||
type: json
|
||||
content: |
|
||||
{
|
||||
"user_token": "user_jg7ng4kdt2x5y", // newuser946
|
||||
"payments_namespace": "artcraft"
|
||||
}
|
||||
order: 4100053395280358
|
||||
+2
-2
@@ -89,7 +89,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_serialization_text_to_image() {
|
||||
let request = EstimateImageCostRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: GenerationProvider::Artcraft,
|
||||
generation_mode: GenerationMode::TextToImage,
|
||||
aspect_ratio: None,
|
||||
@@ -103,7 +103,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_serialization_image_edit() {
|
||||
let request = EstimateImageCostRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: GenerationProvider::Artcraft,
|
||||
generation_mode: GenerationMode::ImageEdit { count: 2 },
|
||||
aspect_ratio: None,
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ pub struct OmniGenImageModelDetails {
|
||||
impl Default for OmniGenImageModelDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
full_name: None,
|
||||
text_prompt_supported: None,
|
||||
text_prompt_max_length: None,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum AudioListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
/// Forward-compatible character reference list.
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum CharacterListRef<'a> {
|
||||
CharacterTokens(&'a Vec<CharacterToken>),
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ pub enum CommonImageModel {
|
||||
#[serde(rename = "gpt_image_1p5")]
|
||||
GptImage1p5,
|
||||
#[serde(rename = "nano_banana")]
|
||||
NanaBanana,
|
||||
NanoBanana,
|
||||
#[serde(rename = "nano_banana_2")]
|
||||
NanaBanana2,
|
||||
NanoBanana2,
|
||||
#[serde(rename = "nano_banana_pro")]
|
||||
NanaBananaPro,
|
||||
NanoBananaPro,
|
||||
#[serde(rename = "seedream_4")]
|
||||
Seedream4,
|
||||
#[serde(rename = "seedream_4p5")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ImageListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ImageRef<'a> {
|
||||
MediaFileToken(&'a MediaFileToken),
|
||||
Url(&'a str),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum VideoListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ mod tests {
|
||||
|
||||
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBanana,
|
||||
model: CommonImageModel::NanoBanana,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ mod tests {
|
||||
|
||||
fn estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBanana2,
|
||||
model: CommonImageModel::NanoBanana2,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ mod tests {
|
||||
|
||||
fn estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::PlanFalFlux1Dev;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_1_dev(
|
||||
plan: &PlanFalFlux1Dev<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.025 per megapixel, billed by rounding up to nearest megapixel.
|
||||
// Default image sizes are ~1MP (1024x1024), so ~$0.025/image = 3 cents rounded up.
|
||||
//
|
||||
// Fal docs:
|
||||
// Your request will cost $0.025 per megapixel, with images billed by rounding up
|
||||
// to the nearest megapixel.
|
||||
//
|
||||
let cost_per_image: u64 = 3;
|
||||
let num_images = plan.num_images.as_u64();
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::PlanFalFlux1Schnell;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_1_schnell(
|
||||
plan: &PlanFalFlux1Schnell<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.003 per megapixel, billed by rounding up to nearest megapixel.
|
||||
// Default image sizes are ~1MP (1024x1024), so ~$0.003/image = 1 cent rounded up.
|
||||
//
|
||||
// Fal docs:
|
||||
// Your request will cost $0.003 per megapixel, with images billed by rounding up
|
||||
// to the nearest megapixel.
|
||||
//
|
||||
let cost_per_image: u64 = 1;
|
||||
let num_images = plan.num_images.as_u64();
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::{
|
||||
FalNbNumImages, PlanFalNanoBanana,
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_nano_banana(
|
||||
plan: &PlanFalNanoBanana<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.039/image (no resolution multiplier — Gemini 25 Flash has no resolution option).
|
||||
// Rounded to 4 cents per image.
|
||||
//
|
||||
// Fal docs:
|
||||
// Your request will cost $0.039 per image. For $1.00, you can run this model 25 times.
|
||||
//
|
||||
let cost_per_image: u64 = 4;
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
FalNbNumImages::One => 1,
|
||||
FalNbNumImages::Two => 2,
|
||||
FalNbNumImages::Three => 3,
|
||||
FalNbNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::{
|
||||
FalNb2NumImages, FalNb2Resolution, PlanFalNanaBanana2,
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_nano_banana_2(
|
||||
plan: &PlanFalNanaBanana2<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.08/image base (1K). Multipliers: 0.5K = 0.75x, 2K = 1.5x, 4K = 2x.
|
||||
// (Stored as USD cents.)
|
||||
//
|
||||
// Fal docs:
|
||||
// Your request will cost $0.08 per image. For $1.00, you can run this model 12 times.
|
||||
// 2K and 4K outputs will be charged at 1.5 times and 2 times the standard rate, respectively.
|
||||
// 0.5K (512px) resolution outputs will be charged at 0.75 times the standard rate. If web
|
||||
// search is used, an additional $0.015 will be charged. If high thinking is used,
|
||||
// an additional $0.002 will be charged. Note: Pricing is subject to change.
|
||||
//
|
||||
let cost_per_image: u64 = match plan.resolution {
|
||||
Some(FalNb2Resolution::HalfK) => 6, // $0.06 = $0.08 * 0.75
|
||||
Some(FalNb2Resolution::OneK) => 8, // $0.08 base
|
||||
Some(FalNb2Resolution::TwoK) => 12, // $0.12 = $0.08 * 1.5
|
||||
Some(FalNb2Resolution::FourK) => 16, // $0.16 = $0.08 * 2.0
|
||||
None => 8, // default = 1K base
|
||||
};
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
FalNb2NumImages::One => 1,
|
||||
FalNb2NumImages::Two => 2,
|
||||
FalNb2NumImages::Three => 3,
|
||||
FalNb2NumImages::Four => 4,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -42,7 +42,7 @@ mod tests {
|
||||
|
||||
fn estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: Provider::Fal,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
|
||||
+4
@@ -1 +1,5 @@
|
||||
pub(crate) mod estimate_image_cost_fal_flux_1_dev;
|
||||
pub(crate) mod estimate_image_cost_fal_flux_1_schnell;
|
||||
pub(crate) mod estimate_image_cost_fal_nano_banana;
|
||||
pub(crate) mod estimate_image_cost_fal_nano_banana_2;
|
||||
pub(crate) mod estimate_image_cost_fal_nano_banana_pro;
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::PlanFalFlux1Dev;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_flux_1_dev_edit_image_webhook::{
|
||||
enqueue_flux_1_dev_edit_image_webhook, Flux1DevEditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_1_dev_text_to_image_webhook::{
|
||||
enqueue_flux_1_dev_text_to_image_webhook, Flux1DevArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_flux_1_dev(
|
||||
plan: &PlanFalFlux1Dev<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if let Some(image_url) = &plan.maybe_image_url {
|
||||
// Image-to-image mode
|
||||
let args = Flux1DevEditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_url: image_url.clone(),
|
||||
num_images: plan.num_images.to_edit(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_flux_1_dev_edit_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
// Text-to-image mode
|
||||
let args = Flux1DevArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
num_images: plan.num_images.to_t2i(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_flux_1_dev_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::PlanFalFlux1Schnell;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_flux_1_schnell_edit_image_webhook::{
|
||||
enqueue_flux_1_schnell_edit_image_webhook, Flux1SchnellEditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_1_schnell_text_to_image_webhook::{
|
||||
enqueue_flux_1_schnell_text_to_image_webhook, Flux1SchnellArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_flux_1_schnell(
|
||||
plan: &PlanFalFlux1Schnell<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if let Some(image_url) = &plan.maybe_image_url {
|
||||
// Image-to-image (redux) mode
|
||||
let args = Flux1SchnellEditImageArgs {
|
||||
image_url: image_url.clone(),
|
||||
num_images: plan.num_images.to_edit(),
|
||||
image_size: plan.edit_image_size,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_flux_1_schnell_edit_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
// Text-to-image mode
|
||||
let args = Flux1SchnellArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
aspect_ratio: plan.t2i_aspect_ratio,
|
||||
num_images: plan.num_images.to_t2i(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_flux_1_schnell_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::{
|
||||
FalNbNumImages, PlanFalNanoBanana,
|
||||
};
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{
|
||||
enqueue_gemini_25_flash_edit_webhook, Gemini25FlashEditArgs,
|
||||
Gemini25FlashEditNumImages,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::{
|
||||
enqueue_gemini_25_flash_text_to_image_webhook, Gemini25FlashTextToImageArgs,
|
||||
Gemini25FlashTextToImageNumImages,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_nano_banana(
|
||||
plan: &PlanFalNanoBanana<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
// Text-to-image mode
|
||||
let args = Gemini25FlashTextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
num_images: to_t2i_num_images(plan.num_images),
|
||||
aspect_ratio: plan.t2i_aspect_ratio,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_gemini_25_flash_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
// Image-edit mode
|
||||
let args = Gemini25FlashEditArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: to_edit_num_images(plan.num_images),
|
||||
aspect_ratio: plan.edit_aspect_ratio,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_gemini_25_flash_edit_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_t2i_num_images(n: FalNbNumImages) -> Gemini25FlashTextToImageNumImages {
|
||||
match n {
|
||||
FalNbNumImages::One => Gemini25FlashTextToImageNumImages::One,
|
||||
FalNbNumImages::Two => Gemini25FlashTextToImageNumImages::Two,
|
||||
FalNbNumImages::Three => Gemini25FlashTextToImageNumImages::Three,
|
||||
FalNbNumImages::Four => Gemini25FlashTextToImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_num_images(n: FalNbNumImages) -> Gemini25FlashEditNumImages {
|
||||
match n {
|
||||
FalNbNumImages::One => Gemini25FlashEditNumImages::One,
|
||||
FalNbNumImages::Two => Gemini25FlashEditNumImages::Two,
|
||||
FalNbNumImages::Three => Gemini25FlashEditNumImages::Three,
|
||||
FalNbNumImages::Four => Gemini25FlashEditNumImages::Four,
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::{
|
||||
FalNb2NumImages, FalNb2Resolution, PlanFalNanaBanana2,
|
||||
};
|
||||
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_2_edit_image_webhook::{
|
||||
enqueue_nano_banana_2_edit_image_webhook, EnqueueNanoBanana2EditImageArgs,
|
||||
EnqueueNanoBanana2EditImageNumImages, EnqueueNanoBanana2EditImageResolution,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_nano_banana_2_text_to_image_webhook::{
|
||||
enqueue_nano_banana_2_text_to_image_webhook, EnqueueNanoBanana2TextToImageArgs,
|
||||
EnqueueNanoBanana2TextToImageNumImages, EnqueueNanoBanana2TextToImageResolution,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_nano_banana_2(
|
||||
plan: &PlanFalNanaBanana2<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
// Text-to-image mode
|
||||
let args = EnqueueNanoBanana2TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
num_images: to_t2i_num_images(plan.num_images),
|
||||
resolution: plan.resolution.map(to_t2i_resolution),
|
||||
aspect_ratio: plan.t2i_aspect_ratio,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_nano_banana_2_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
// Image-edit mode
|
||||
let args = EnqueueNanoBanana2EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: to_edit_num_images(plan.num_images),
|
||||
resolution: plan.resolution.map(to_edit_resolution),
|
||||
aspect_ratio: plan.edit_aspect_ratio,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_nano_banana_2_edit_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_t2i_num_images(n: FalNb2NumImages) -> EnqueueNanoBanana2TextToImageNumImages {
|
||||
match n {
|
||||
FalNb2NumImages::One => EnqueueNanoBanana2TextToImageNumImages::One,
|
||||
FalNb2NumImages::Two => EnqueueNanoBanana2TextToImageNumImages::Two,
|
||||
FalNb2NumImages::Three => EnqueueNanoBanana2TextToImageNumImages::Three,
|
||||
FalNb2NumImages::Four => EnqueueNanoBanana2TextToImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_num_images(n: FalNb2NumImages) -> EnqueueNanoBanana2EditImageNumImages {
|
||||
match n {
|
||||
FalNb2NumImages::One => EnqueueNanoBanana2EditImageNumImages::One,
|
||||
FalNb2NumImages::Two => EnqueueNanoBanana2EditImageNumImages::Two,
|
||||
FalNb2NumImages::Three => EnqueueNanoBanana2EditImageNumImages::Three,
|
||||
FalNb2NumImages::Four => EnqueueNanoBanana2EditImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_resolution(r: FalNb2Resolution) -> EnqueueNanoBanana2TextToImageResolution {
|
||||
match r {
|
||||
FalNb2Resolution::HalfK => EnqueueNanoBanana2TextToImageResolution::HalfK,
|
||||
FalNb2Resolution::OneK => EnqueueNanoBanana2TextToImageResolution::OneK,
|
||||
FalNb2Resolution::TwoK => EnqueueNanoBanana2TextToImageResolution::TwoK,
|
||||
FalNb2Resolution::FourK => EnqueueNanoBanana2TextToImageResolution::FourK,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_resolution(r: FalNb2Resolution) -> EnqueueNanoBanana2EditImageResolution {
|
||||
match r {
|
||||
FalNb2Resolution::HalfK => EnqueueNanoBanana2EditImageResolution::HalfK,
|
||||
FalNb2Resolution::OneK => EnqueueNanoBanana2EditImageResolution::OneK,
|
||||
FalNb2Resolution::TwoK => EnqueueNanoBanana2EditImageResolution::TwoK,
|
||||
FalNb2Resolution::FourK => EnqueueNanoBanana2EditImageResolution::FourK,
|
||||
}
|
||||
}
|
||||
+4
@@ -1 +1,5 @@
|
||||
pub mod generate_image_fal_flux_1_dev;
|
||||
pub mod generate_image_fal_flux_1_schnell;
|
||||
pub mod generate_image_fal_nano_banana;
|
||||
pub mod generate_image_fal_nano_banana_2;
|
||||
pub mod generate_image_fal_nano_banana_pro;
|
||||
|
||||
+15
-6
@@ -10,19 +10,24 @@ use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::plan_generate_image_artcraft_flux_1_dev;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_schnell::plan_generate_image_artcraft_flux_1_schnell;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::plan_generate_image_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1::plan_generate_image_artcraft_flux_pro_1p1;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::plan_generate_image_artcraft_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::plan_generate_image_artcraft_gpt_image_1p5;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::plan_generate_image_artcraft_nano_banana;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::plan_generate_image_artcraft_nano_banana_2;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::plan_generate_image_artcraft_nano_banana_pro;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::plan_generate_image_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::plan_generate_image_artcraft_seedream_4;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::plan_generate_image_artcraft_seedream_4p5;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::plan_generate_image_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::plan_generate_image_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::plan_generate_image_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::plan_generate_image_fal_flux_1_dev;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::plan_generate_image_fal_flux_1_schnell;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::plan_generate_image_fal_nano_banana;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::plan_generate_image_fal_nano_banana_2;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::plan_generate_image_fal_nano_banana_pro;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GenerateImageRequest<'a> {
|
||||
/// Which model to use.
|
||||
pub model: CommonImageModel,
|
||||
@@ -86,9 +91,9 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
CommonImageModel::FluxPro11 => plan_generate_image_artcraft_flux_pro_1p1(self),
|
||||
CommonImageModel::FluxPro11Ultra => plan_generate_image_artcraft_flux_pro_1p1_ultra(self),
|
||||
CommonImageModel::GptImage1p5 => plan_generate_image_artcraft_gpt_image_1p5(self),
|
||||
CommonImageModel::NanaBanana => plan_generate_image_artcraft_nano_banana(self),
|
||||
CommonImageModel::NanaBanana2 => plan_generate_image_artcraft_nano_banana_2(self),
|
||||
CommonImageModel::NanaBananaPro => plan_generate_image_artcraft_nano_banana_pro(self),
|
||||
CommonImageModel::NanoBanana => plan_generate_image_artcraft_nano_banana(self),
|
||||
CommonImageModel::NanoBanana2 => plan_generate_image_artcraft_nano_banana_2(self),
|
||||
CommonImageModel::NanoBananaPro => plan_generate_image_artcraft_nano_banana_pro(self),
|
||||
CommonImageModel::Seedream4 => plan_generate_image_artcraft_seedream_4(self),
|
||||
CommonImageModel::Seedream4p5 => plan_generate_image_artcraft_seedream_4p5(self),
|
||||
CommonImageModel::Seedream5Lite => plan_generate_image_artcraft_seedream_5_lite(self),
|
||||
@@ -99,7 +104,11 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
|
||||
fn build_fal(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.model {
|
||||
CommonImageModel::NanaBananaPro => plan_generate_image_fal_nano_banana_pro(self),
|
||||
CommonImageModel::Flux1Dev => plan_generate_image_fal_flux_1_dev(self),
|
||||
CommonImageModel::Flux1Schnell => plan_generate_image_fal_flux_1_schnell(self),
|
||||
CommonImageModel::NanoBanana => plan_generate_image_fal_nano_banana(self),
|
||||
CommonImageModel::NanoBanana2 => plan_generate_image_fal_nano_banana_2(self),
|
||||
CommonImageModel::NanoBananaPro => plan_generate_image_fal_nano_banana_pro(self),
|
||||
_ => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "provider",
|
||||
|
||||
+44
@@ -13,6 +13,10 @@ use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraf
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_5_lite::estimate_image_cost_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_qwen_edit_2511_angles::estimate_image_cost_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_2_lora_angles::estimate_image_cost_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_1_dev::estimate_image_cost_fal_flux_1_dev;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_1_schnell::estimate_image_cost_fal_flux_1_schnell;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana::estimate_image_cost_fal_nano_banana;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_2::estimate_image_cost_fal_nano_banana_2;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_pro::estimate_image_cost_fal_nano_banana_pro;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_dev::execute_artcraft_flux_1_dev;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_schnell::execute_artcraft_flux_1_schnell;
|
||||
@@ -27,6 +31,10 @@ use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_5_lite::execute_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_qwen_edit_2511_angles::execute_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_2_lora_angles::execute_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_1_dev::execute_fal_flux_1_dev;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_1_schnell::execute_fal_flux_1_schnell;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana::execute_fal_nano_banana;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_2::execute_fal_nano_banana_2;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_pro::execute_fal_nano_banana_pro;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
@@ -43,6 +51,10 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::PlanFalFlux1Dev;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::PlanFalFlux1Schnell;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::PlanFalNanoBanana;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::PlanFalNanaBanana2;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::PlanFalNanaBananaPro;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -60,6 +72,10 @@ pub enum ImageGenerationPlan<'a> {
|
||||
ArtcraftSeedream5Lite(PlanArtcraftSeedream5Lite<'a>),
|
||||
ArtcraftQwenEdit2511Angles(PlanArtcraftQwenEdit2511Angles<'a>),
|
||||
ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles<'a>),
|
||||
FalFlux1Dev(PlanFalFlux1Dev<'a>),
|
||||
FalFlux1Schnell(PlanFalFlux1Schnell<'a>),
|
||||
FalNanoBanana(PlanFalNanoBanana<'a>),
|
||||
FalNanaBanana2(PlanFalNanaBanana2<'a>),
|
||||
FalNanaBananaPro(PlanFalNanaBananaPro<'a>),
|
||||
}
|
||||
|
||||
@@ -121,6 +137,22 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_flux_2_lora_angles(plan, artcraft_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalFlux1Dev(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_flux_1_dev(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalFlux1Schnell(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_flux_1_schnell(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalNanoBanana(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_nano_banana(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalNanaBanana2(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_nano_banana_2(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalNanaBananaPro(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_nano_banana_pro(plan, fal_client).await
|
||||
@@ -169,6 +201,18 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) => {
|
||||
estimate_image_cost_artcraft_flux_2_lora_angles(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalFlux1Dev(plan) => {
|
||||
estimate_image_cost_fal_flux_1_dev(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalFlux1Schnell(plan) => {
|
||||
estimate_image_cost_fal_flux_1_schnell(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalNanoBanana(plan) => {
|
||||
estimate_image_cost_fal_nano_banana(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalNanaBanana2(plan) => {
|
||||
estimate_image_cost_fal_nano_banana_2(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalNanaBananaPro(plan) => {
|
||||
estimate_image_cost_fal_nano_banana_pro(plan)
|
||||
}
|
||||
|
||||
+4
@@ -1 +1,5 @@
|
||||
pub mod plan_generate_image_fal_flux_1_dev;
|
||||
pub mod plan_generate_image_fal_flux_1_schnell;
|
||||
pub mod plan_generate_image_fal_nano_banana;
|
||||
pub mod plan_generate_image_fal_nano_banana_2;
|
||||
pub mod plan_generate_image_fal_nano_banana_pro;
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_flux_1_dev_edit_image_webhook::Flux1DevEditImageNumImages;
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_1_dev_text_to_image_webhook::{
|
||||
Flux1DevAspectRatio, Flux1DevNumImages,
|
||||
};
|
||||
|
||||
/// Shared num-images type for both t2i and edit modes.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalFlux1DevNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalFlux1Dev<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Image URL for editing. None = text-to-image mode.
|
||||
pub maybe_image_url: Option<String>,
|
||||
pub aspect_ratio: Flux1DevAspectRatio,
|
||||
pub num_images: FalFlux1DevNumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_flux_1_dev<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let maybe_image_url = resolve_single_image_url(request.image_inputs)?;
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalFlux1Dev(PlanFalFlux1Dev {
|
||||
prompt: request.prompt,
|
||||
maybe_image_url,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_single_image_url(
|
||||
image_inputs: Option<ImageListRef<'_>>,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::Urls(urls)) => {
|
||||
if urls.is_empty() {
|
||||
Ok(None)
|
||||
} else if urls.len() == 1 {
|
||||
Ok(Some(urls[0].clone()))
|
||||
} else {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Flux 1 Dev image-to-image supports exactly 1 image, got {}", urls.len()),
|
||||
}))
|
||||
}
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Flux1DevAspectRatio, ArtcraftRouterError> {
|
||||
use Flux1DevAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Ar::SquareHd),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::PortraitNineBySixteen),
|
||||
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Ar::PortraitNineBySixteen),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Ar::Square),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalFlux1DevNumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalFlux1DevNumImages::One),
|
||||
2 => Ok(FalFlux1DevNumImages::Two),
|
||||
3 => Ok(FalFlux1DevNumImages::Three),
|
||||
4 => Ok(FalFlux1DevNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalFlux1DevNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalFlux1DevNumImages {
|
||||
pub fn to_t2i(self) -> Flux1DevNumImages {
|
||||
match self {
|
||||
Self::One => Flux1DevNumImages::One,
|
||||
Self::Two => Flux1DevNumImages::Two,
|
||||
Self::Three => Flux1DevNumImages::Three,
|
||||
Self::Four => Flux1DevNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> Flux1DevEditImageNumImages {
|
||||
match self {
|
||||
Self::One => Flux1DevEditImageNumImages::One,
|
||||
Self::Two => Flux1DevEditImageNumImages::Two,
|
||||
Self::Three => Flux1DevEditImageNumImages::Three,
|
||||
Self::Four => Flux1DevEditImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_flux_1_schnell_edit_image_webhook::{
|
||||
Flux1SchnellEditImageNumImages, Flux1SchnellEditImageSize,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_1_schnell_text_to_image_webhook::{
|
||||
Flux1SchnellAspectRatio, Flux1SchnellNumImages,
|
||||
};
|
||||
|
||||
/// Shared num-images type for both t2i and edit modes.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalFlux1SchnellNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalFlux1Schnell<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Image URL for editing. None = text-to-image mode.
|
||||
pub maybe_image_url: Option<String>,
|
||||
pub t2i_aspect_ratio: Flux1SchnellAspectRatio,
|
||||
pub edit_image_size: Option<Flux1SchnellEditImageSize>,
|
||||
pub num_images: FalFlux1SchnellNumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_flux_1_schnell<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let maybe_image_url = resolve_single_image_url(request.image_inputs)?;
|
||||
let t2i_aspect_ratio = plan_t2i_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let edit_image_size = plan_edit_image_size(request.aspect_ratio);
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalFlux1Schnell(PlanFalFlux1Schnell {
|
||||
prompt: request.prompt,
|
||||
maybe_image_url,
|
||||
t2i_aspect_ratio,
|
||||
edit_image_size,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_single_image_url(
|
||||
image_inputs: Option<ImageListRef<'_>>,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::Urls(urls)) => {
|
||||
if urls.is_empty() {
|
||||
Ok(None)
|
||||
} else if urls.len() == 1 {
|
||||
Ok(Some(urls[0].clone()))
|
||||
} else {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Flux 1 Schnell redux supports exactly 1 image, got {}", urls.len()),
|
||||
}))
|
||||
}
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_t2i_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Flux1SchnellAspectRatio, ArtcraftRouterError> {
|
||||
use Flux1SchnellAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto4k) => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::Square) => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Ar::SquareHd),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::PortraitNineBySixteen),
|
||||
Some(CommonAspectRatio::WideFiveByFour) | Some(CommonAspectRatio::WideThreeByTwo) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallFourByFive) | Some(CommonAspectRatio::TallTwoByThree) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Ar::PortraitNineBySixteen),
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Ar::Square),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_edit_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<Flux1SchnellEditImageSize> {
|
||||
use Flux1SchnellEditImageSize as Size;
|
||||
match aspect_ratio {
|
||||
None => None,
|
||||
Some(CommonAspectRatio::Square) => Some(Size::Square),
|
||||
Some(CommonAspectRatio::SquareHd) => Some(Size::SquareHd),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Some(Size::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Some(Size::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Some(Size::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Some(Size::PortraitNineBySixteen),
|
||||
_ => Some(Size::Square),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalFlux1SchnellNumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalFlux1SchnellNumImages::One),
|
||||
2 => Ok(FalFlux1SchnellNumImages::Two),
|
||||
3 => Ok(FalFlux1SchnellNumImages::Three),
|
||||
4 => Ok(FalFlux1SchnellNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalFlux1SchnellNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalFlux1SchnellNumImages {
|
||||
pub fn to_t2i(self) -> Flux1SchnellNumImages {
|
||||
match self {
|
||||
Self::One => Flux1SchnellNumImages::One,
|
||||
Self::Two => Flux1SchnellNumImages::Two,
|
||||
Self::Three => Flux1SchnellNumImages::Three,
|
||||
Self::Four => Flux1SchnellNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> Flux1SchnellEditImageNumImages {
|
||||
match self {
|
||||
Self::One => Flux1SchnellEditImageNumImages::One,
|
||||
Self::Two => Flux1SchnellEditImageNumImages::Two,
|
||||
Self::Three => Flux1SchnellEditImageNumImages::Three,
|
||||
Self::Four => Flux1SchnellEditImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::Gemini25FlashEditAspectRatio;
|
||||
use fal_client::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::Gemini25FlashTextToImageAspectRatio;
|
||||
|
||||
/// Intermediate num-images type shared between t2i and edit enums.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalNbNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalNanoBanana<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Image URLs for editing. Empty vec = text-to-image mode.
|
||||
pub image_urls: Vec<String>,
|
||||
/// Pre-resolved aspect ratio for text-to-image mode.
|
||||
pub t2i_aspect_ratio: Option<Gemini25FlashTextToImageAspectRatio>,
|
||||
/// Pre-resolved aspect ratio for image-edit mode.
|
||||
pub edit_aspect_ratio: Option<Gemini25FlashEditAspectRatio>,
|
||||
/// Pre-resolved number of images (1–4, shared for both modes).
|
||||
pub num_images: FalNbNumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_nano_banana<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
let image_urls = resolve_image_list_ref(request.image_inputs)?;
|
||||
let t2i_aspect_ratio = plan_t2i_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let edit_aspect_ratio = plan_edit_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalNanoBanana(PlanFalNanoBanana {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
t2i_aspect_ratio,
|
||||
edit_aspect_ratio,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef<'_>>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gemini 25 Flash text-to-image supported aspect ratios:
|
||||
// 1:1, 5:4, 4:3, 3:2, 16:9, 21:9, 4:5, 3:4, 2:3, 9:16
|
||||
// (no Auto for text-to-image)
|
||||
fn plan_t2i_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Gemini25FlashTextToImageAspectRatio>, ArtcraftRouterError> {
|
||||
use Gemini25FlashTextToImageAspectRatio as T2iAr;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
// Auto not valid for text-to-image; fall back to square
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Some(T2iAr::OneByOne)),
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(T2iAr::OneByOne)),
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(T2iAr::FiveByFour)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(T2iAr::FourByThree)),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(T2iAr::ThreeByTwo)),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(T2iAr::SixteenByNine)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(T2iAr::TwentyOneByNine)),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(T2iAr::FourByFive)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(T2iAr::ThreeByFour)),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(T2iAr::TwoByThree)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(T2iAr::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(T2iAr::NineBySixteen))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Gemini 25 Flash image-edit supported aspect ratios: same as t2i plus Auto
|
||||
fn plan_edit_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
is_edit_mode: bool,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Gemini25FlashEditAspectRatio>, ArtcraftRouterError> {
|
||||
use Gemini25FlashEditAspectRatio as EditAr;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => {
|
||||
if is_edit_mode {
|
||||
Ok(Some(EditAr::Auto))
|
||||
} else {
|
||||
Ok(Some(EditAr::OneByOne))
|
||||
}
|
||||
}
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(EditAr::OneByOne)),
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(EditAr::FiveByFour)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(EditAr::FourByThree)),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(EditAr::ThreeByTwo)),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(EditAr::SixteenByNine)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(EditAr::TwentyOneByNine)),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(EditAr::FourByFive)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(EditAr::ThreeByFour)),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(EditAr::TwoByThree)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(EditAr::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(EditAr::NineBySixteen))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalNbNumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalNbNumImages::One),
|
||||
2 => Ok(FalNbNumImages::Two),
|
||||
3 => Ok(FalNbNumImages::Three),
|
||||
4 => Ok(FalNbNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(FalNbNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_2_edit_image_webhook::EnqueueNanoBanana2EditImageAspectRatio;
|
||||
use fal_client::requests::webhook::image::text::enqueue_nano_banana_2_text_to_image_webhook::EnqueueNanoBanana2TextToImageAspectRatio;
|
||||
|
||||
/// Intermediate resolution type shared between t2i and edit enums.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalNb2Resolution {
|
||||
HalfK,
|
||||
OneK,
|
||||
TwoK,
|
||||
FourK,
|
||||
}
|
||||
|
||||
/// Intermediate num-images type shared between t2i and edit enums.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalNb2NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalNanaBanana2<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Image URLs for editing. Empty vec = text-to-image mode.
|
||||
pub image_urls: Vec<String>,
|
||||
/// Pre-resolved aspect ratio for text-to-image mode.
|
||||
pub t2i_aspect_ratio: Option<EnqueueNanoBanana2TextToImageAspectRatio>,
|
||||
/// Pre-resolved aspect ratio for image-edit mode.
|
||||
pub edit_aspect_ratio: Option<EnqueueNanoBanana2EditImageAspectRatio>,
|
||||
/// Pre-resolved resolution (shared for both modes).
|
||||
pub resolution: Option<FalNb2Resolution>,
|
||||
/// Pre-resolved number of images (1–4, shared for both modes).
|
||||
pub num_images: FalNb2NumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_nano_banana_2<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
let image_urls = resolve_image_list_ref(request.image_inputs)?;
|
||||
let t2i_aspect_ratio = plan_t2i_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let edit_aspect_ratio = plan_edit_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let resolution = plan_resolution(request.resolution)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalNanaBanana2(PlanFalNanaBanana2 {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
t2i_aspect_ratio,
|
||||
edit_aspect_ratio,
|
||||
resolution,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef<'_>>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NB2 text-to-image supported aspect ratios:
|
||||
// auto, 1:1, 5:4, 4:3, 3:2, 16:9, 21:9, 4:5, 3:4, 2:3, 9:16
|
||||
fn plan_t2i_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<EnqueueNanoBanana2TextToImageAspectRatio>, ArtcraftRouterError> {
|
||||
use EnqueueNanoBanana2TextToImageAspectRatio as T2iAr;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Some(T2iAr::Auto)),
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(T2iAr::OneByOne)),
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(T2iAr::FiveByFour)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(T2iAr::FourByThree)),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(T2iAr::ThreeByTwo)),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(T2iAr::SixteenByNine)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(T2iAr::TwentyOneByNine)),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(T2iAr::FourByFive)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(T2iAr::ThreeByFour)),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(T2iAr::TwoByThree)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(T2iAr::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(T2iAr::NineBySixteen))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NB2 image-edit supported aspect ratios: same as t2i plus Auto
|
||||
fn plan_edit_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
is_edit_mode: bool,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<EnqueueNanoBanana2EditImageAspectRatio>, ArtcraftRouterError> {
|
||||
use EnqueueNanoBanana2EditImageAspectRatio as EditAr;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => {
|
||||
if is_edit_mode {
|
||||
Ok(Some(EditAr::Auto))
|
||||
} else {
|
||||
Ok(Some(EditAr::OneByOne))
|
||||
}
|
||||
}
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(EditAr::OneByOne)),
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(EditAr::FiveByFour)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(EditAr::FourByThree)),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(EditAr::ThreeByTwo)),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(EditAr::SixteenByNine)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(EditAr::TwentyOneByNine)),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(EditAr::FourByFive)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(EditAr::ThreeByFour)),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(EditAr::TwoByThree)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(EditAr::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(EditAr::NineBySixteen))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
) -> Result<Option<FalNb2Resolution>, ArtcraftRouterError> {
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
Some(CommonResolution::HalfK) => Ok(Some(FalNb2Resolution::HalfK)),
|
||||
Some(CommonResolution::OneK) => Ok(Some(FalNb2Resolution::OneK)),
|
||||
Some(CommonResolution::TwoK) => Ok(Some(FalNb2Resolution::TwoK)),
|
||||
Some(CommonResolution::FourK) => Ok(Some(FalNb2Resolution::FourK)),
|
||||
Some(CommonResolution::ThreeK) => Ok(Some(FalNb2Resolution::TwoK)),
|
||||
Some(CommonResolution::FourEightyP) => Ok(Some(FalNb2Resolution::HalfK)),
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(Some(FalNb2Resolution::OneK)),
|
||||
Some(CommonResolution::TenEightyP) => Ok(Some(FalNb2Resolution::OneK)),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalNb2NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalNb2NumImages::One),
|
||||
2 => Ok(FalNb2NumImages::Two),
|
||||
3 => Ok(FalNb2NumImages::Three),
|
||||
4 => Ok(FalNb2NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(FalNb2NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -250,7 +250,7 @@ mod tests {
|
||||
|
||||
fn base_fal_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space"),
|
||||
image_inputs: None,
|
||||
|
||||
+3
-3
@@ -1,9 +1,8 @@
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
@@ -21,6 +20,7 @@ use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
/// Plan to either (1) generate a video or (2) determine how much it costs to generate that video.
|
||||
/// This works across multiple providers by shaping a generic "GenerateVideoRequest" into a provider-specific plan.
|
||||
/// That plan can then be used to return a cost estimate for that given provider or return a struct that can be used to send a real generation request.
|
||||
#[derive(Debug)]
|
||||
pub struct GenerateVideoRequest<'a> {
|
||||
/// Which model to use.
|
||||
pub model: CommonVideoModel,
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn get_artcraft_client() -> RouterClient {
|
||||
|
||||
pub fn base_image_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
image_inputs: None,
|
||||
@@ -49,7 +49,7 @@ pub fn get_fal_client() -> RouterClient {
|
||||
|
||||
pub fn base_fal_image_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space"),
|
||||
image_inputs: None,
|
||||
@@ -121,7 +121,7 @@ pub fn base_seedream_5_lite_image_request() -> GenerateImageRequest<'static> {
|
||||
|
||||
pub fn base_nano_banana_2_image_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBanana2,
|
||||
model: CommonImageModel::NanoBanana2,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
image_inputs: None,
|
||||
@@ -139,7 +139,7 @@ pub fn base_nano_banana_2_image_request() -> GenerateImageRequest<'static> {
|
||||
|
||||
pub fn base_nano_banana_image_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBanana,
|
||||
model: CommonImageModel::NanoBanana,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
image_inputs: None,
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
use crate::requests::api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Flux1DevEditImageInput {
|
||||
pub prompt: String,
|
||||
|
||||
pub image_url: String,
|
||||
|
||||
/// 0.0 - 1.0
|
||||
/// Default: 0.95
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub strength: Option<f64>,
|
||||
|
||||
/// 1 - 4
|
||||
/// Default: 1
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_images: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub seed: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub guidance_scale: Option<f64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_inference_steps: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_safety_checker: Option<bool>,
|
||||
|
||||
/// Options: jpeg, png
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sync_mode: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Flux1DevEditImageFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Flux1DevEditImageOutput {
|
||||
pub images: Vec<Flux1DevEditImageFile>,
|
||||
}
|
||||
|
||||
pub fn flux_1_dev_edit_image(
|
||||
params: Flux1DevEditImageInput,
|
||||
) -> FalRequest<Flux1DevEditImageInput, Flux1DevEditImageOutput> {
|
||||
FalRequest::new("fal-ai/flux/dev/image-to-image", params)
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
use crate::requests::api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Flux1SchnellEditImageInput {
|
||||
pub image_url: String,
|
||||
|
||||
/// Options: square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_size: Option<String>,
|
||||
|
||||
/// 1 - 4
|
||||
/// Default: 1
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_images: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub seed: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_inference_steps: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_safety_checker: Option<bool>,
|
||||
|
||||
/// Options: jpeg, png
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sync_mode: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Flux1SchnellEditImageFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Flux1SchnellEditImageOutput {
|
||||
pub images: Vec<Flux1SchnellEditImageFile>,
|
||||
}
|
||||
|
||||
pub fn flux_1_schnell_edit_image(
|
||||
params: Flux1SchnellEditImageInput,
|
||||
) -> FalRequest<Flux1SchnellEditImageInput, Flux1SchnellEditImageOutput> {
|
||||
FalRequest::new("fal-ai/flux/schnell/redux", params)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod http_flux_1_dev_edit_image;
|
||||
pub mod http_flux_1_schnell_edit_image;
|
||||
pub mod http_flux_pro_kontext_max_edit;
|
||||
pub mod http_gemini_25_flash_edit;
|
||||
pub mod http_gpt_image_1_edit_image;
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::http::image::edit::http_flux_1_dev_edit_image::{flux_1_dev_edit_image, Flux1DevEditImageInput};
|
||||
use crate::requests::api::webhook_response::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct Flux1DevEditImageArgs<'a, U: IntoUrl> {
|
||||
pub prompt: &'a str,
|
||||
pub image_url: String,
|
||||
pub num_images: Flux1DevEditImageNumImages,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: U,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Flux1DevEditImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
pub async fn enqueue_flux_1_dev_edit_image_webhook<U: IntoUrl>(
|
||||
args: Flux1DevEditImageArgs<'_, U>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
let num_images = match args.num_images {
|
||||
Flux1DevEditImageNumImages::One => 1,
|
||||
Flux1DevEditImageNumImages::Two => 2,
|
||||
Flux1DevEditImageNumImages::Three => 3,
|
||||
Flux1DevEditImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let request = Flux1DevEditImageInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
image_url: args.image_url,
|
||||
num_images: Some(num_images),
|
||||
enable_safety_checker: Some(false),
|
||||
output_format: Some("png".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = flux_1_dev_edit_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::edit::enqueue_flux_1_dev_edit_image_webhook::{
|
||||
enqueue_flux_1_dev_edit_image_webhook, Flux1DevEditImageArgs, Flux1DevEditImageNumImages,
|
||||
};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::GHOST_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real Fal API request
|
||||
async fn test_single_image() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Flux1DevEditImageArgs {
|
||||
prompt: "make this image look like a watercolor painting",
|
||||
image_url: GHOST_IMAGE_URL.to_string(),
|
||||
num_images: Flux1DevEditImageNumImages::One,
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_flux_1_dev_edit_image_webhook(args).await?;
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real Fal API request
|
||||
async fn test_batch_two() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Flux1DevEditImageArgs {
|
||||
prompt: "turn this into a cyberpunk scene with neon lights",
|
||||
image_url: GHOST_IMAGE_URL.to_string(),
|
||||
num_images: Flux1DevEditImageNumImages::Two,
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_flux_1_dev_edit_image_webhook(args).await?;
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::http::image::edit::http_flux_1_schnell_edit_image::{flux_1_schnell_edit_image, Flux1SchnellEditImageInput};
|
||||
use crate::requests::api::webhook_response::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct Flux1SchnellEditImageArgs<'a, U: IntoUrl> {
|
||||
pub image_url: String,
|
||||
pub num_images: Flux1SchnellEditImageNumImages,
|
||||
pub image_size: Option<Flux1SchnellEditImageSize>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: U,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Flux1SchnellEditImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Flux1SchnellEditImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
LandscapeFourByThree,
|
||||
LandscapeSixteenByNine,
|
||||
PortraitThreeByFour,
|
||||
PortraitNineBySixteen,
|
||||
}
|
||||
|
||||
pub async fn enqueue_flux_1_schnell_edit_image_webhook<U: IntoUrl>(
|
||||
args: Flux1SchnellEditImageArgs<'_, U>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
let num_images = match args.num_images {
|
||||
Flux1SchnellEditImageNumImages::One => 1,
|
||||
Flux1SchnellEditImageNumImages::Two => 2,
|
||||
Flux1SchnellEditImageNumImages::Three => 3,
|
||||
Flux1SchnellEditImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let image_size = args.image_size.map(|s| match s {
|
||||
Flux1SchnellEditImageSize::Square => "square",
|
||||
Flux1SchnellEditImageSize::SquareHd => "square_hd",
|
||||
Flux1SchnellEditImageSize::LandscapeFourByThree => "landscape_4_3",
|
||||
Flux1SchnellEditImageSize::LandscapeSixteenByNine => "landscape_16_9",
|
||||
Flux1SchnellEditImageSize::PortraitThreeByFour => "portrait_4_3",
|
||||
Flux1SchnellEditImageSize::PortraitNineBySixteen => "portrait_16_9",
|
||||
}.to_string());
|
||||
|
||||
let request = Flux1SchnellEditImageInput {
|
||||
image_url: args.image_url,
|
||||
num_images: Some(num_images),
|
||||
image_size,
|
||||
enable_safety_checker: Some(false),
|
||||
output_format: Some("png".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = flux_1_schnell_edit_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::edit::enqueue_flux_1_schnell_edit_image_webhook::{
|
||||
enqueue_flux_1_schnell_edit_image_webhook, Flux1SchnellEditImageArgs,
|
||||
Flux1SchnellEditImageNumImages, Flux1SchnellEditImageSize,
|
||||
};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::GHOST_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real Fal API request
|
||||
async fn test_single_image_no_size() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Flux1SchnellEditImageArgs {
|
||||
image_url: GHOST_IMAGE_URL.to_string(),
|
||||
num_images: Flux1SchnellEditImageNumImages::One,
|
||||
image_size: None,
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_flux_1_schnell_edit_image_webhook(args).await?;
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real Fal API request
|
||||
async fn test_with_landscape_size() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Flux1SchnellEditImageArgs {
|
||||
image_url: GHOST_IMAGE_URL.to_string(),
|
||||
num_images: Flux1SchnellEditImageNumImages::Two,
|
||||
image_size: Some(Flux1SchnellEditImageSize::LandscapeSixteenByNine),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_flux_1_schnell_edit_image_webhook(args).await?;
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod enqueue_flux_1_dev_edit_image_webhook;
|
||||
pub mod enqueue_flux_1_schnell_edit_image_webhook;
|
||||
pub mod enqueue_bytedance_seedream_v4_edit_image_webhook;
|
||||
pub mod enqueue_bytedance_seedream_v4p5_edit_image_webhook;
|
||||
pub mod enqueue_bytedance_seedream_v5_lite_edit_image_webhook;
|
||||
|
||||
+3
-3
@@ -23,10 +23,10 @@ pub async fn handle_image_edit_artcraft(
|
||||
match model {
|
||||
ImageEditModel::FluxProKontextMax => handle_artcraft_flux_kontext_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await,
|
||||
ImageEditModel::Gemini25Flash | ImageEditModel::NanoBanana => {
|
||||
handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBanana, GenerationModel::NanoBanana).await
|
||||
handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanoBanana, GenerationModel::NanoBanana).await
|
||||
},
|
||||
ImageEditModel::NanoBanana2 => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBanana2, GenerationModel::NanoBanana2).await,
|
||||
ImageEditModel::NanoBananaPro => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBananaPro, GenerationModel::NanoBananaPro).await,
|
||||
ImageEditModel::NanoBanana2 => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanoBanana2, GenerationModel::NanoBanana2).await,
|
||||
ImageEditModel::NanoBananaPro => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanoBananaPro, GenerationModel::NanoBananaPro).await,
|
||||
ImageEditModel::GptImage1 => handle_artcraft_gpt_image_1_edit(request, app, app_data_root, app_env_configs, storyteller_creds_manager).await,
|
||||
ImageEditModel::GptImage1p5 => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::GptImage1p5, GenerationModel::GptImage1p5).await,
|
||||
ImageEditModel::Seedream4 => handle_image_edit_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream4, GenerationModel::Seedream4).await,
|
||||
|
||||
+3
-3
@@ -38,10 +38,10 @@ pub async fn handle_text_to_image_artcraft(
|
||||
TextToImageModel::GptImage1 => handle_artcraft_gpt_image_1_text_to_image(request, app_env_configs, storyteller_creds_manager).await,
|
||||
TextToImageModel::GptImage1p5 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::GptImage1p5, GenerationModel::GptImage1p5).await,
|
||||
TextToImageModel::Gemini25Flash | TextToImageModel::NanoBanana => {
|
||||
handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBanana, GenerationModel::NanoBanana).await
|
||||
handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanoBanana, GenerationModel::NanoBanana).await
|
||||
},
|
||||
TextToImageModel::NanoBanana2 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBanana2, GenerationModel::NanoBanana2).await,
|
||||
TextToImageModel::NanoBananaPro => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanaBananaPro, GenerationModel::NanoBananaPro).await,
|
||||
TextToImageModel::NanoBanana2 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanoBanana2, GenerationModel::NanoBanana2).await,
|
||||
TextToImageModel::NanoBananaPro => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::NanoBananaPro, GenerationModel::NanoBananaPro).await,
|
||||
TextToImageModel::Seedream4 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream4, GenerationModel::Seedream4).await,
|
||||
TextToImageModel::Seedream4p5 => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream4p5, GenerationModel::Seedream4p5).await,
|
||||
TextToImageModel::Seedream5Lite => handle_text_to_image_artcraft_via_router(request, app_env_configs, storyteller_creds_manager, CommonImageModel::Seedream5Lite, GenerationModel::Seedream5Lite).await,
|
||||
|
||||
@@ -16,11 +16,11 @@ pub enum CommonImageModel {
|
||||
#[serde(rename = "gpt_image_1p5")]
|
||||
GptImage1p5,
|
||||
#[serde(rename = "nano_banana")]
|
||||
NanaBanana,
|
||||
NanoBanana,
|
||||
#[serde(rename = "nano_banana_2")]
|
||||
NanaBanana2,
|
||||
NanoBanana2,
|
||||
#[serde(rename = "nano_banana_pro")]
|
||||
NanaBananaPro,
|
||||
NanoBananaPro,
|
||||
#[serde(rename = "seedream_4")]
|
||||
Seedream4,
|
||||
#[serde(rename = "seedream_4p5")]
|
||||
@@ -28,3 +28,48 @@ pub enum CommonImageModel {
|
||||
#[serde(rename = "seedream_5_lite")]
|
||||
Seedream5Lite,
|
||||
}
|
||||
|
||||
impl CommonImageModel {
|
||||
pub fn to_common_model_type(&self) -> crate::common::generation::common_model_type::CommonModelType {
|
||||
use crate::common::generation::common_model_type::CommonModelType;
|
||||
match self {
|
||||
Self::Flux1Dev => CommonModelType::Flux1Dev,
|
||||
Self::Flux1Schnell => CommonModelType::Flux1Schnell,
|
||||
Self::FluxPro11 => CommonModelType::FluxPro11,
|
||||
Self::FluxPro11Ultra => CommonModelType::FluxPro11Ultra,
|
||||
Self::GptImage1p5 => CommonModelType::GptImage1p5,
|
||||
Self::NanoBanana => CommonModelType::NanoBanana,
|
||||
Self::NanoBanana2 => CommonModelType::NanoBanana2,
|
||||
Self::NanoBananaPro => CommonModelType::NanoBananaPro,
|
||||
Self::Seedream4 => CommonModelType::Seedream4,
|
||||
Self::Seedream4p5 => CommonModelType::Seedream4p5,
|
||||
Self::Seedream5Lite => CommonModelType::Seedream5Lite,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::common::generation::common_model_type::CommonModelType;
|
||||
|
||||
#[test]
|
||||
fn all_image_models_convert_to_common_model_type() {
|
||||
let models = [
|
||||
(CommonImageModel::Flux1Dev, CommonModelType::Flux1Dev),
|
||||
(CommonImageModel::Flux1Schnell, CommonModelType::Flux1Schnell),
|
||||
(CommonImageModel::FluxPro11, CommonModelType::FluxPro11),
|
||||
(CommonImageModel::FluxPro11Ultra, CommonModelType::FluxPro11Ultra),
|
||||
(CommonImageModel::GptImage1p5, CommonModelType::GptImage1p5),
|
||||
(CommonImageModel::NanoBanana, CommonModelType::NanoBanana),
|
||||
(CommonImageModel::NanoBanana2, CommonModelType::NanoBanana2),
|
||||
(CommonImageModel::NanoBananaPro, CommonModelType::NanoBananaPro),
|
||||
(CommonImageModel::Seedream4, CommonModelType::Seedream4),
|
||||
(CommonImageModel::Seedream4p5, CommonModelType::Seedream4p5),
|
||||
(CommonImageModel::Seedream5Lite, CommonModelType::Seedream5Lite),
|
||||
];
|
||||
for (image_model, expected) in models {
|
||||
assert_eq!(image_model.to_common_model_type(), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,3 +59,62 @@ pub enum CommonVideoModel {
|
||||
#[serde(rename = "veo_3p1_fast")]
|
||||
Veo3p1Fast,
|
||||
}
|
||||
|
||||
impl CommonVideoModel {
|
||||
pub fn to_common_model_type(&self) -> crate::common::generation::common_model_type::CommonModelType {
|
||||
use crate::common::generation::common_model_type::CommonModelType;
|
||||
match self {
|
||||
Self::GrokVideo => CommonModelType::GrokVideo,
|
||||
Self::Kling16Pro => CommonModelType::Kling16Pro,
|
||||
Self::Kling21Pro => CommonModelType::Kling21Pro,
|
||||
Self::Kling21Master => CommonModelType::Kling21Master,
|
||||
Self::Kling2p5TurboPro => CommonModelType::Kling2p5TurboPro,
|
||||
Self::Kling2p6Pro => CommonModelType::Kling2p6Pro,
|
||||
Self::Kling3p0Standard => CommonModelType::Kling3p0Standard,
|
||||
Self::Kling3p0Pro => CommonModelType::Kling3p0Pro,
|
||||
Self::Seedance10Lite => CommonModelType::Seedance10Lite,
|
||||
Self::Seedance1p5Pro => CommonModelType::Seedance1p5Pro,
|
||||
Self::Seedance2p0 => CommonModelType::Seedance2p0,
|
||||
Self::Sora2 => CommonModelType::Sora2,
|
||||
Self::Sora2Pro => CommonModelType::Sora2Pro,
|
||||
Self::Veo2 => CommonModelType::Veo2,
|
||||
Self::Veo3 => CommonModelType::Veo3,
|
||||
Self::Veo3Fast => CommonModelType::Veo3Fast,
|
||||
Self::Veo3p1 => CommonModelType::Veo3p1,
|
||||
Self::Veo3p1Fast => CommonModelType::Veo3p1Fast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::common::generation::common_model_type::CommonModelType;
|
||||
|
||||
#[test]
|
||||
fn all_video_models_convert_to_common_model_type() {
|
||||
let models = [
|
||||
(CommonVideoModel::GrokVideo, CommonModelType::GrokVideo),
|
||||
(CommonVideoModel::Kling16Pro, CommonModelType::Kling16Pro),
|
||||
(CommonVideoModel::Kling21Pro, CommonModelType::Kling21Pro),
|
||||
(CommonVideoModel::Kling21Master, CommonModelType::Kling21Master),
|
||||
(CommonVideoModel::Kling2p5TurboPro, CommonModelType::Kling2p5TurboPro),
|
||||
(CommonVideoModel::Kling2p6Pro, CommonModelType::Kling2p6Pro),
|
||||
(CommonVideoModel::Kling3p0Standard, CommonModelType::Kling3p0Standard),
|
||||
(CommonVideoModel::Kling3p0Pro, CommonModelType::Kling3p0Pro),
|
||||
(CommonVideoModel::Seedance10Lite, CommonModelType::Seedance10Lite),
|
||||
(CommonVideoModel::Seedance1p5Pro, CommonModelType::Seedance1p5Pro),
|
||||
(CommonVideoModel::Seedance2p0, CommonModelType::Seedance2p0),
|
||||
(CommonVideoModel::Sora2, CommonModelType::Sora2),
|
||||
(CommonVideoModel::Sora2Pro, CommonModelType::Sora2Pro),
|
||||
(CommonVideoModel::Veo2, CommonModelType::Veo2),
|
||||
(CommonVideoModel::Veo3, CommonModelType::Veo3),
|
||||
(CommonVideoModel::Veo3Fast, CommonModelType::Veo3Fast),
|
||||
(CommonVideoModel::Veo3p1, CommonModelType::Veo3p1),
|
||||
(CommonVideoModel::Veo3p1Fast, CommonModelType::Veo3p1Fast),
|
||||
];
|
||||
for (video_model, expected) in models {
|
||||
assert_eq!(video_model.to_common_model_type(), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
// ...
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::NanaBanana, // NB: currently Gemini25Flash in our system
|
||||
model: CommonImageModel::NanoBanana, // NB: currently Gemini25Flash in our system
|
||||
full_name: None,
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
@@ -85,7 +85,7 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::NanaBanana2,
|
||||
model: CommonImageModel::NanoBanana2,
|
||||
full_name: None,
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
@@ -118,7 +118,7 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
full_name: None,
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
@@ -167,15 +167,15 @@ fn build_omni_gen_image_model_providers() -> Vec<OmniGenImageModelProviderDetail
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenImageProviderModelDetails {
|
||||
model: CommonImageModel::NanaBanana,
|
||||
model: CommonImageModel::NanoBanana,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenImageProviderModelDetails {
|
||||
model: CommonImageModel::NanaBanana2,
|
||||
model: CommonImageModel::NanoBanana2,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenImageProviderModelDetails {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
overrides: None,
|
||||
},
|
||||
],
|
||||
|
||||
+3
-3
@@ -130,9 +130,9 @@ fn map_image_model(model: CommonImageModel) -> Result<RouterImageModel, HandlerE
|
||||
CommonImageModel::FluxPro11 => RouterImageModel::FluxPro11,
|
||||
CommonImageModel::FluxPro11Ultra => RouterImageModel::FluxPro11Ultra,
|
||||
CommonImageModel::GptImage1p5 => RouterImageModel::GptImage1p5,
|
||||
CommonImageModel::NanaBanana => RouterImageModel::NanaBanana,
|
||||
CommonImageModel::NanaBanana2 => RouterImageModel::NanaBanana2,
|
||||
CommonImageModel::NanaBananaPro => RouterImageModel::NanaBananaPro,
|
||||
CommonImageModel::NanoBanana => RouterImageModel::NanoBanana,
|
||||
CommonImageModel::NanoBanana2 => RouterImageModel::NanoBanana2,
|
||||
CommonImageModel::NanoBananaPro => RouterImageModel::NanoBananaPro,
|
||||
CommonImageModel::Seedream4 => RouterImageModel::Seedream4,
|
||||
CommonImageModel::Seedream4p5 => RouterImageModel::Seedream4p5,
|
||||
CommonImageModel::Seedream5Lite => RouterImageModel::Seedream5Lite,
|
||||
|
||||
+26
-6
@@ -2,11 +2,13 @@ use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::cost_response::omni_gen_image_cost_response::OmniGenImageCostResponse;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use log::warn;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::endpoints::omni_gen::generate::image::transform_request::transform_request;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Estimate the cost of an image generation.
|
||||
@@ -18,14 +20,32 @@ use crate::state::server_state::ServerState;
|
||||
responses(
|
||||
(status = 200, description = "Success", body = OmniGenImageCostResponse),
|
||||
(status = 400, description = "Bad input"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn omni_gen_image_cost_handler(
|
||||
_http_request: HttpRequest,
|
||||
_request: Json<OmniGenImageCostAndGenerateRequest>,
|
||||
request: Json<OmniGenImageCostAndGenerateRequest>,
|
||||
_server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<OmniGenImageCostResponse>, CommonWebError> {
|
||||
todo!()
|
||||
) -> Result<Json<OmniGenImageCostResponse>, AdvancedCommonWebError> {
|
||||
let mut generate_request = transform_request(&request)?;
|
||||
generate_request.provider = Provider::Artcraft;
|
||||
|
||||
let plan = generate_request.build()
|
||||
.map_err(|e| {
|
||||
warn!("Failed to build cost plan: {}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
let estimate = plan.estimate_costs();
|
||||
|
||||
Ok(Json(OmniGenImageCostResponse {
|
||||
success: true,
|
||||
cost_in_credits: estimate.cost_in_credits,
|
||||
cost_in_usd_cents: estimate.cost_in_usd_cents,
|
||||
is_free: estimate.is_free,
|
||||
is_unlimited: estimate.is_unlimited,
|
||||
is_rate_limited: estimate.is_rate_limited,
|
||||
has_watermark: estimate.has_watermark,
|
||||
}))
|
||||
}
|
||||
|
||||
+26
-6
@@ -2,11 +2,13 @@ use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::cost_response::omni_gen_video_cost_response::OmniGenVideoCostResponse;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use log::warn;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::transform_request::transform_request;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Estimate the cost of a video generation.
|
||||
@@ -18,14 +20,32 @@ use crate::state::server_state::ServerState;
|
||||
responses(
|
||||
(status = 200, description = "Success", body = OmniGenVideoCostResponse),
|
||||
(status = 400, description = "Bad input"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn omni_gen_video_cost_handler(
|
||||
_http_request: HttpRequest,
|
||||
_request: Json<OmniGenVideoCostAndGenerateRequest>,
|
||||
request: Json<OmniGenVideoCostAndGenerateRequest>,
|
||||
_server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<OmniGenVideoCostResponse>, CommonWebError> {
|
||||
todo!()
|
||||
) -> Result<Json<OmniGenVideoCostResponse>, AdvancedCommonWebError> {
|
||||
let mut generate_request = transform_request(&request)?;
|
||||
generate_request.provider = Provider::Artcraft;
|
||||
|
||||
let plan = generate_request.build()
|
||||
.map_err(|e| {
|
||||
warn!("Failed to build cost plan: {}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
let estimate = plan.estimate_costs();
|
||||
|
||||
Ok(Json(OmniGenVideoCostResponse {
|
||||
success: true,
|
||||
cost_in_credits: estimate.cost_in_credits,
|
||||
cost_in_usd_cents: estimate.cost_in_usd_cents,
|
||||
is_free: estimate.is_free,
|
||||
is_unlimited: estimate.is_unlimited,
|
||||
is_rate_limited: estimate.is_rate_limited,
|
||||
has_watermark: estimate.has_watermark,
|
||||
}))
|
||||
}
|
||||
|
||||
+4
@@ -1 +1,5 @@
|
||||
pub mod omni_gen_image_generate_handler;
|
||||
mod request_to_costs;
|
||||
mod request_to_plan;
|
||||
mod resolve_media_tokens;
|
||||
pub(crate) mod transform_request;
|
||||
|
||||
+256
-6
@@ -2,13 +2,41 @@ use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::{error, info, warn};
|
||||
use sqlx::Acquire;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::generate_response::omni_gen_image_generate_response::OmniGenImageGenerateResponse;
|
||||
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::visibility::Visibility;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue_with_apriori_job_token::{
|
||||
insert_generic_inference_job_for_fal_queue_with_apriori_job_token,
|
||||
InsertGenericInferenceForFalWithAprioriJobTokenArgs,
|
||||
};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
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 tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
use super::request_to_costs::request_to_costs;
|
||||
use super::request_to_plan::request_to_plan;
|
||||
use super::resolve_media_tokens::{resolve_media_tokens, apply_resolved_media};
|
||||
use super::transform_request::transform_request;
|
||||
|
||||
/// Generate an image using the omni-gen unified endpoint.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
@@ -24,9 +52,231 @@ use crate::state::server_state::ServerState;
|
||||
),
|
||||
)]
|
||||
pub async fn omni_gen_image_generate_handler(
|
||||
_http_request: HttpRequest,
|
||||
_request: Json<OmniGenImageCostAndGenerateRequest>,
|
||||
_server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<OmniGenImageGenerateResponse>, CommonWebError> {
|
||||
todo!()
|
||||
http_request: HttpRequest,
|
||||
request: Json<OmniGenImageCostAndGenerateRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<OmniGenImageGenerateResponse>, AdvancedCommonWebError> {
|
||||
|
||||
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
|
||||
|
||||
// ==================== TRANSFORM REQUEST + PLAN ==================== //
|
||||
|
||||
let mut generate_request = transform_request(&request)?;
|
||||
|
||||
let maybe_prompt_model_type: Option<CommonModelType> = request.model
|
||||
.as_ref()
|
||||
.map(|m| m.to_common_model_type());
|
||||
|
||||
// ==================== COST ==================== //
|
||||
|
||||
let cost_estimate = request_to_costs(&generate_request)?;
|
||||
|
||||
// ==================== SESSION ==================== //
|
||||
|
||||
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);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
let user_token = match maybe_user_session.as_ref() {
|
||||
Some(session) => &session.user_token,
|
||||
None => return Err(AdvancedCommonWebError::NotAuthorized),
|
||||
};
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
// ==================== IDEMPOTENCY ==================== //
|
||||
|
||||
let idempotency_token = request.idempotency_token.as_deref()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&idempotency_token) {
|
||||
return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&idempotency_token, &mut *mysql_connection)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
|
||||
})?;
|
||||
|
||||
// ==================== RESOLVE MEDIA TOKENS ==================== //
|
||||
|
||||
let resolved_media = resolve_media_tokens(
|
||||
&request,
|
||||
&http_request,
|
||||
&mut mysql_connection,
|
||||
server_state.server_environment,
|
||||
).await?;
|
||||
|
||||
apply_resolved_media(&mut generate_request, &resolved_media);
|
||||
|
||||
// ==================== PLAN ==================== //
|
||||
|
||||
let plan = request_to_plan(&mut generate_request)?;
|
||||
|
||||
// ==================== BILLING ==================== //
|
||||
|
||||
let cost = cost_estimate.cost_in_credits.unwrap_or(0);
|
||||
|
||||
info!("Charging wallet: {} credits", cost);
|
||||
|
||||
let apriori_job_token = InferenceJobToken::generate();
|
||||
|
||||
if cost > 0 {
|
||||
attempt_wallet_deduction_else_common_web_error(
|
||||
user_token,
|
||||
Some(apriori_job_token.as_str()),
|
||||
cost,
|
||||
&mut mysql_connection,
|
||||
).await?;
|
||||
}
|
||||
|
||||
// ==================== EXECUTE GENERATION ==================== //
|
||||
|
||||
let fal_client = artcraft_router::client::router_fal_client::RouterFalClient::new(
|
||||
server_state.fal.api_key.clone(),
|
||||
server_state.fal.webhook_url.clone(),
|
||||
);
|
||||
|
||||
let router_client = artcraft_router::client::router_client::RouterClient::Fal(fal_client);
|
||||
|
||||
let generation_response = plan.generate_image(&router_client)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Image generation failed: {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
let external_job_id = match &generation_response {
|
||||
artcraft_router::generate::generate_image::generate_image_response::GenerateImageResponse::Artcraft(p) => {
|
||||
p.inference_job_token.as_str().to_string()
|
||||
}
|
||||
artcraft_router::generate::generate_image::generate_image_response::GenerateImageResponse::Fal(p) => {
|
||||
p.request_id.clone().unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== DB TRANSACTION ==================== //
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let mut transaction = mysql_connection
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error starting MySQL transaction: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
// -- Prompt --
|
||||
|
||||
let generation_mode = if request.image_media_tokens.is_some() {
|
||||
CommonGenerationMode::Edit
|
||||
} else {
|
||||
CommonGenerationMode::Text
|
||||
};
|
||||
|
||||
let prompt_result = insert_prompt(InsertPromptArgs {
|
||||
maybe_apriori_prompt_token: None,
|
||||
prompt_type: PromptType::ArtcraftApp,
|
||||
maybe_creator_user_token: Some(user_token),
|
||||
maybe_model_type: maybe_prompt_model_type,
|
||||
maybe_generation_provider: Some(GenerationProvider::Artcraft),
|
||||
maybe_positive_prompt: request.prompt.as_deref(),
|
||||
maybe_negative_prompt: None,
|
||||
maybe_other_args: None,
|
||||
maybe_generation_mode: Some(generation_mode),
|
||||
maybe_aspect_ratio: None,
|
||||
maybe_resolution: None,
|
||||
maybe_batch_count: request.image_batch_count.map(|c| c as u8),
|
||||
maybe_generate_audio: None,
|
||||
maybe_duration_seconds: 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
|
||||
}
|
||||
};
|
||||
|
||||
// -- Prompt context items --
|
||||
|
||||
if let Some(token) = prompt_token.as_ref() {
|
||||
let mut context_items = Vec::new();
|
||||
|
||||
if let Some(ref_tokens) = &request.image_media_tokens {
|
||||
for media_token in ref_tokens {
|
||||
context_items.push(PromptContextItem {
|
||||
media_token: media_token.clone(),
|
||||
context_semantic_type: PromptContextSemanticType::Imgsrc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if !context_items.is_empty() {
|
||||
if let Err(err) = insert_batch_prompt_context_items(InsertBatchArgs {
|
||||
prompt_token: token.clone(),
|
||||
items: context_items,
|
||||
transaction: &mut transaction,
|
||||
}).await {
|
||||
warn!("Error inserting batch prompt context items: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Inference job --
|
||||
|
||||
let db_result = insert_generic_inference_job_for_fal_queue_with_apriori_job_token(
|
||||
InsertGenericInferenceForFalWithAprioriJobTokenArgs {
|
||||
apriori_job_token: &apriori_job_token,
|
||||
uuid_idempotency_token: &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: Some(user_token),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_executor: &mut *transaction,
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
phantom: Default::default(),
|
||||
}
|
||||
).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting inference job: {:?}", err);
|
||||
return Err(AdvancedCommonWebError::from_error(err));
|
||||
}
|
||||
};
|
||||
|
||||
transaction.commit().await.map_err(|err| {
|
||||
error!("Error committing transaction: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
Ok(Json(OmniGenImageGenerateResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token,
|
||||
}))
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use artcraft_router::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use log::warn;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
|
||||
/// Estimate costs for an image generation request.
|
||||
/// Always uses the Artcraft provider for costing regardless of the execution provider.
|
||||
pub fn request_to_costs(
|
||||
request: &GenerateImageRequest<'_>,
|
||||
) -> Result<ImageGenerationCostEstimate, AdvancedCommonWebError> {
|
||||
let mut cost_request = GenerateImageRequest {
|
||||
provider: Provider::Artcraft,
|
||||
..*request
|
||||
};
|
||||
|
||||
let plan = cost_request.build()
|
||||
.map_err(|e| {
|
||||
warn!("Failed to build cost plan: {}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
Ok(plan.estimate_costs())
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use artcraft_router::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use log::warn;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
|
||||
/// Build an image generation plan from a transformed request.
|
||||
/// Overrides the provider to Fal for execution (costing uses Artcraft separately).
|
||||
pub fn request_to_plan<'a>(
|
||||
request: &'a mut GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, AdvancedCommonWebError> {
|
||||
request.provider = Provider::Fal;
|
||||
|
||||
request.build()
|
||||
.map_err(|e| {
|
||||
warn!("Failed to build image generation plan: {}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_web::HttpRequest;
|
||||
use log::info;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::MySql;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::util::lookup::lookup_image_urls_as_map::lookup_image_urls_as_map;
|
||||
use server_environment::ServerEnvironment;
|
||||
|
||||
/// Resolved media URLs that can be referenced by a GenerateImageRequest.
|
||||
/// The owned Vecs here outlive the request so the GenerateImageRequest can borrow them.
|
||||
pub struct ResolvedImageMedia {
|
||||
pub url_map: HashMap<MediaFileToken, String>,
|
||||
pub image_input_urls: Vec<String>,
|
||||
}
|
||||
|
||||
/// Collect all media file tokens from the raw HTTP request, query them from the database,
|
||||
/// and return resolved CDN URLs.
|
||||
pub async fn resolve_media_tokens(
|
||||
omni_request: &OmniGenImageCostAndGenerateRequest,
|
||||
http_request: &HttpRequest,
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
server_environment: ServerEnvironment,
|
||||
) -> Result<ResolvedImageMedia, AdvancedCommonWebError> {
|
||||
let mut all_tokens: Vec<MediaFileToken> = Vec::new();
|
||||
|
||||
if let Some(tokens) = &omni_request.image_media_tokens {
|
||||
all_tokens.extend(tokens.iter().cloned());
|
||||
}
|
||||
|
||||
if all_tokens.is_empty() {
|
||||
return Ok(ResolvedImageMedia {
|
||||
url_map: HashMap::new(),
|
||||
image_input_urls: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
info!("Resolving {} media file tokens to CDN URLs", all_tokens.len());
|
||||
|
||||
let url_map = lookup_image_urls_as_map(
|
||||
http_request,
|
||||
mysql_connection,
|
||||
server_environment,
|
||||
&all_tokens,
|
||||
).await?;
|
||||
|
||||
let image_input_urls = omni_request.image_media_tokens.as_ref()
|
||||
.map(|tokens| tokens.iter().filter_map(|t| url_map.get(t).cloned()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(ResolvedImageMedia {
|
||||
url_map,
|
||||
image_input_urls,
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply resolved media URLs to a GenerateImageRequest, replacing MediaFileToken refs with URL refs.
|
||||
pub fn apply_resolved_media<'a>(
|
||||
request: &mut GenerateImageRequest<'a>,
|
||||
resolved: &'a ResolvedImageMedia,
|
||||
) {
|
||||
if !resolved.image_input_urls.is_empty() {
|
||||
request.image_inputs = Some(ImageListRef::Urls(&resolved.image_input_urls));
|
||||
} else {
|
||||
request.image_inputs = None;
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioRouter;
|
||||
use artcraft_router::api::common_image_model::CommonImageModel as CommonImageModelRouter;
|
||||
use artcraft_router::api::common_resolution::CommonResolution as CommonResolutionRouter;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
|
||||
fn convert_model(
|
||||
model: &CommonImageModelEnum,
|
||||
) -> Result<CommonImageModelRouter, AdvancedCommonWebError> {
|
||||
let json = serde_json::to_string(model)?;
|
||||
serde_json::from_str(&json).map_err(|e| {
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
format!("Unsupported image model: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_aspect_ratio(
|
||||
ar: &CommonAspectRatioEnum,
|
||||
) -> Result<CommonAspectRatioRouter, AdvancedCommonWebError> {
|
||||
let json = serde_json::to_string(ar)?;
|
||||
serde_json::from_str(&json).map_err(|e| {
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
format!("Unsupported aspect ratio: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_resolution(
|
||||
res: &CommonResolutionEnum,
|
||||
) -> Result<CommonResolutionRouter, AdvancedCommonWebError> {
|
||||
let json = serde_json::to_string(res)?;
|
||||
serde_json::from_str(&json).map_err(|e| {
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
format!("Unsupported resolution: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn transform_request(
|
||||
request: &OmniGenImageCostAndGenerateRequest,
|
||||
) -> Result<GenerateImageRequest<'_>, AdvancedCommonWebError> {
|
||||
let api_model = request.model.as_ref()
|
||||
.ok_or_else(|| AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
"model is required".to_string(),
|
||||
))?;
|
||||
|
||||
let model = convert_model(api_model)?;
|
||||
|
||||
let aspect_ratio = request.aspect_ratio.as_ref()
|
||||
.map(convert_aspect_ratio)
|
||||
.transpose()?;
|
||||
|
||||
let resolution = request.resolution.as_ref()
|
||||
.map(convert_resolution)
|
||||
.transpose()?;
|
||||
|
||||
Ok(GenerateImageRequest {
|
||||
model,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: request.prompt.as_deref(),
|
||||
image_inputs: request.image_media_tokens.as_ref()
|
||||
.map(ImageListRef::MediaFileTokens),
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
image_batch_count: request.image_batch_count,
|
||||
horizontal_angle: request.horizontal_angle,
|
||||
vertical_angle: request.vertical_angle,
|
||||
zoom: request.zoom,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: request.idempotency_token.as_deref(),
|
||||
})
|
||||
}
|
||||
+4
@@ -1 +1,5 @@
|
||||
pub mod omni_gen_video_generate_handler;
|
||||
mod request_to_costs;
|
||||
mod request_to_plan;
|
||||
mod resolve_media_tokens;
|
||||
pub(crate) mod transform_request;
|
||||
|
||||
+281
-6
@@ -2,13 +2,41 @@ use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::{error, info, warn};
|
||||
use sqlx::Acquire;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::generate_response::omni_gen_video_generate_response::OmniGenVideoGenerateResponse;
|
||||
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::visibility::Visibility;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue_with_apriori_job_token::{
|
||||
insert_generic_inference_job_for_fal_queue_with_apriori_job_token,
|
||||
InsertGenericInferenceForFalWithAprioriJobTokenArgs,
|
||||
};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
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 tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
use super::request_to_costs::request_to_costs;
|
||||
use super::request_to_plan::request_to_plan;
|
||||
use super::resolve_media_tokens::{resolve_media_tokens, apply_resolved_media};
|
||||
use super::transform_request::transform_request;
|
||||
|
||||
/// Generate a video using the omni-gen unified endpoint.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
@@ -24,9 +52,256 @@ use crate::state::server_state::ServerState;
|
||||
),
|
||||
)]
|
||||
pub async fn omni_gen_video_generate_handler(
|
||||
_http_request: HttpRequest,
|
||||
_request: Json<OmniGenVideoCostAndGenerateRequest>,
|
||||
_server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<OmniGenVideoGenerateResponse>, CommonWebError> {
|
||||
todo!()
|
||||
http_request: HttpRequest,
|
||||
request: Json<OmniGenVideoCostAndGenerateRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<OmniGenVideoGenerateResponse>, AdvancedCommonWebError> {
|
||||
|
||||
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
|
||||
|
||||
// ==================== TRANSFORM REQUEST + PLAN ==================== //
|
||||
|
||||
let mut generate_request = transform_request(&request)?;
|
||||
|
||||
let maybe_prompt_model_type: Option<CommonModelType> = request.model
|
||||
.as_ref()
|
||||
.map(|m| m.to_common_model_type());
|
||||
|
||||
// ==================== COST ==================== //
|
||||
|
||||
let cost_estimate = request_to_costs(&generate_request)?;
|
||||
|
||||
// ==================== SESSION ==================== //
|
||||
|
||||
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);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
let user_token = match maybe_user_session.as_ref() {
|
||||
Some(session) => &session.user_token,
|
||||
None => return Err(AdvancedCommonWebError::NotAuthorized),
|
||||
};
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
// ==================== IDEMPOTENCY ==================== //
|
||||
|
||||
let idempotency_token = request.idempotency_token.as_deref()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&idempotency_token) {
|
||||
return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
|
||||
insert_idempotency_token(&idempotency_token, &mut *mysql_connection)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error inserting idempotency token: {:?}", err);
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
|
||||
})?;
|
||||
|
||||
// ==================== RESOLVE MEDIA TOKENS ==================== //
|
||||
|
||||
let resolved_media = resolve_media_tokens(
|
||||
&request,
|
||||
&http_request,
|
||||
&mut mysql_connection,
|
||||
server_state.server_environment,
|
||||
).await?;
|
||||
|
||||
apply_resolved_media(&mut generate_request, &resolved_media);
|
||||
|
||||
// ==================== PLAN ==================== //
|
||||
|
||||
let plan = request_to_plan(&mut generate_request)?;
|
||||
|
||||
// ==================== BILLING ==================== //
|
||||
|
||||
let cost = cost_estimate.cost_in_credits.unwrap_or(0);
|
||||
|
||||
info!("Charging wallet: {} credits", cost);
|
||||
|
||||
let apriori_job_token = InferenceJobToken::generate();
|
||||
|
||||
if cost > 0 {
|
||||
attempt_wallet_deduction_else_common_web_error(
|
||||
user_token,
|
||||
Some(apriori_job_token.as_str()),
|
||||
cost,
|
||||
&mut mysql_connection,
|
||||
).await?;
|
||||
}
|
||||
|
||||
// ==================== EXECUTE GENERATION ==================== //
|
||||
|
||||
let fal_client = artcraft_router::client::router_fal_client::RouterFalClient::new(
|
||||
server_state.fal.api_key.clone(),
|
||||
server_state.fal.webhook_url.clone(),
|
||||
);
|
||||
let router_client = artcraft_router::client::router_client::RouterClient::Fal(fal_client);
|
||||
|
||||
let generation_response = plan.generate_video(&router_client)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Video generation failed: {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
let external_job_id = match &generation_response {
|
||||
artcraft_router::generate::generate_video::generate_video_response::GenerateVideoResponse::Artcraft(p) => {
|
||||
p.inference_job_token.as_str().to_string()
|
||||
}
|
||||
artcraft_router::generate::generate_video::generate_video_response::GenerateVideoResponse::Muapi(p) => {
|
||||
p.request_id.as_str().to_string()
|
||||
}
|
||||
artcraft_router::generate::generate_video::generate_video_response::GenerateVideoResponse::Seedance2Pro(p) => {
|
||||
p.order_id.clone()
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== DB TRANSACTION ==================== //
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let mut transaction = mysql_connection
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error starting MySQL transaction: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
// -- Prompt --
|
||||
|
||||
let generation_mode = if request.start_frame_image_media_token.is_some() {
|
||||
CommonGenerationMode::Keyframe
|
||||
} else {
|
||||
CommonGenerationMode::Text
|
||||
};
|
||||
|
||||
let prompt_result = insert_prompt(InsertPromptArgs {
|
||||
maybe_apriori_prompt_token: None,
|
||||
prompt_type: PromptType::ArtcraftApp,
|
||||
maybe_creator_user_token: Some(user_token),
|
||||
maybe_model_type: maybe_prompt_model_type,
|
||||
maybe_generation_provider: Some(GenerationProvider::Artcraft),
|
||||
maybe_positive_prompt: request.prompt.as_deref(),
|
||||
maybe_negative_prompt: request.negative_prompt.as_deref(),
|
||||
maybe_other_args: None,
|
||||
maybe_generation_mode: Some(generation_mode),
|
||||
maybe_aspect_ratio: None,
|
||||
maybe_resolution: None,
|
||||
maybe_batch_count: request.video_batch_count.map(|c| c as u8),
|
||||
maybe_generate_audio: request.generate_audio,
|
||||
maybe_duration_seconds: request.duration_seconds.map(|d| d as u32),
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
// -- Prompt context items --
|
||||
|
||||
if let Some(token) = prompt_token.as_ref() {
|
||||
let mut context_items = Vec::new();
|
||||
|
||||
if let Some(media_token) = &request.start_frame_image_media_token {
|
||||
context_items.push(PromptContextItem {
|
||||
media_token: media_token.clone(),
|
||||
context_semantic_type: PromptContextSemanticType::VidStartFrame,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(media_token) = &request.end_frame_image_media_token {
|
||||
context_items.push(PromptContextItem {
|
||||
media_token: media_token.clone(),
|
||||
context_semantic_type: PromptContextSemanticType::VidEndFrame,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(ref_tokens) = &request.reference_image_media_tokens {
|
||||
for media_token in ref_tokens {
|
||||
context_items.push(PromptContextItem {
|
||||
media_token: media_token.clone(),
|
||||
context_semantic_type: PromptContextSemanticType::Imgref,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref_tokens) = &request.reference_video_media_tokens {
|
||||
for media_token in ref_tokens {
|
||||
context_items.push(PromptContextItem {
|
||||
media_token: media_token.clone(),
|
||||
context_semantic_type: PromptContextSemanticType::VidRef,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if !context_items.is_empty() {
|
||||
if let Err(err) = insert_batch_prompt_context_items(InsertBatchArgs {
|
||||
prompt_token: token.clone(),
|
||||
items: context_items,
|
||||
transaction: &mut transaction,
|
||||
}).await {
|
||||
warn!("Error inserting batch prompt context items: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Inference job --
|
||||
|
||||
let db_result = insert_generic_inference_job_for_fal_queue_with_apriori_job_token(
|
||||
InsertGenericInferenceForFalWithAprioriJobTokenArgs {
|
||||
apriori_job_token: &apriori_job_token,
|
||||
uuid_idempotency_token: &idempotency_token,
|
||||
maybe_external_third_party_id: &external_job_id,
|
||||
fal_category: FalCategory::VideoGeneration,
|
||||
maybe_inference_args: None,
|
||||
maybe_prompt_token: prompt_token.as_ref(),
|
||||
maybe_creator_user_token: Some(user_token),
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
mysql_executor: &mut *transaction,
|
||||
starting_job_status_override: None,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
phantom: Default::default(),
|
||||
}
|
||||
).await;
|
||||
|
||||
let job_token = match db_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!("Error inserting inference job: {:?}", err);
|
||||
return Err(AdvancedCommonWebError::from_error(err));
|
||||
}
|
||||
};
|
||||
|
||||
transaction.commit().await.map_err(|err| {
|
||||
error!("Error committing transaction: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
Ok(Json(OmniGenVideoGenerateResponse {
|
||||
success: true,
|
||||
inference_job_token: job_token,
|
||||
}))
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use artcraft_router::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use log::warn;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
|
||||
/// Estimate costs for a video generation request.
|
||||
/// Always uses the Artcraft provider for costing regardless of the execution provider.
|
||||
pub fn request_to_costs(
|
||||
request: &GenerateVideoRequest<'_>,
|
||||
) -> Result<VideoGenerationCostEstimate, AdvancedCommonWebError> {
|
||||
let mut cost_request = GenerateVideoRequest {
|
||||
provider: Provider::Artcraft,
|
||||
..*request
|
||||
};
|
||||
|
||||
let plan = cost_request.build()
|
||||
.map_err(|e| {
|
||||
warn!("Failed to build cost plan: {}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
Ok(plan.estimate_costs())
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use artcraft_router::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use log::warn;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
|
||||
/// Build a video generation plan from a transformed request.
|
||||
/// Overrides the provider to Fal for execution (costing uses Artcraft separately).
|
||||
pub fn request_to_plan<'a>(
|
||||
request: &'a mut GenerateVideoRequest<'a>,
|
||||
) -> Result<VideoGenerationPlan<'a>, AdvancedCommonWebError> {
|
||||
request.provider = Provider::Fal;
|
||||
|
||||
request.build()
|
||||
.map_err(|e| {
|
||||
warn!("Failed to build video generation plan: {}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_web::HttpRequest;
|
||||
use log::info;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::MySql;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_router::api::audio_list_ref::AudioListRef;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
use artcraft_router::api::video_list_ref::VideoListRef;
|
||||
use artcraft_router::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::util::lookup::lookup_image_urls_as_map::lookup_image_urls_as_map;
|
||||
use server_environment::ServerEnvironment;
|
||||
|
||||
/// Resolved media URLs that can be referenced by a GenerateVideoRequest.
|
||||
/// The owned Vecs here outlive the request so the GenerateVideoRequest can borrow them.
|
||||
pub struct ResolvedVideoMedia {
|
||||
pub url_map: HashMap<MediaFileToken, String>,
|
||||
pub maybe_start_frame_url: Option<String>,
|
||||
pub maybe_end_frame_url: Option<String>,
|
||||
pub reference_image_urls: Vec<String>,
|
||||
pub reference_video_urls: Vec<String>,
|
||||
pub reference_audio_urls: Vec<String>,
|
||||
}
|
||||
|
||||
/// Collect all media file tokens from the raw HTTP request, query them from the database,
|
||||
/// and return resolved CDN URLs.
|
||||
pub async fn resolve_media_tokens(
|
||||
omni_request: &OmniGenVideoCostAndGenerateRequest,
|
||||
http_request: &HttpRequest,
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
server_environment: ServerEnvironment,
|
||||
) -> Result<ResolvedVideoMedia, AdvancedCommonWebError> {
|
||||
let mut all_tokens: Vec<MediaFileToken> = Vec::new();
|
||||
|
||||
if let Some(token) = &omni_request.start_frame_image_media_token {
|
||||
all_tokens.push(token.clone());
|
||||
}
|
||||
if let Some(token) = &omni_request.end_frame_image_media_token {
|
||||
all_tokens.push(token.clone());
|
||||
}
|
||||
if let Some(tokens) = &omni_request.reference_image_media_tokens {
|
||||
all_tokens.extend(tokens.iter().cloned());
|
||||
}
|
||||
if let Some(tokens) = &omni_request.reference_video_media_tokens {
|
||||
all_tokens.extend(tokens.iter().cloned());
|
||||
}
|
||||
if let Some(tokens) = &omni_request.reference_audio_media_tokens {
|
||||
all_tokens.extend(tokens.iter().cloned());
|
||||
}
|
||||
|
||||
if all_tokens.is_empty() {
|
||||
return Ok(ResolvedVideoMedia {
|
||||
url_map: HashMap::new(),
|
||||
maybe_start_frame_url: None,
|
||||
maybe_end_frame_url: None,
|
||||
reference_image_urls: Vec::new(),
|
||||
reference_video_urls: Vec::new(),
|
||||
reference_audio_urls: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
info!("Resolving {} media file tokens to CDN URLs", all_tokens.len());
|
||||
|
||||
let url_map = lookup_image_urls_as_map(
|
||||
http_request,
|
||||
mysql_connection,
|
||||
server_environment,
|
||||
&all_tokens,
|
||||
).await?;
|
||||
|
||||
let maybe_start_frame_url = omni_request.start_frame_image_media_token.as_ref()
|
||||
.and_then(|t| url_map.get(t).cloned());
|
||||
|
||||
let maybe_end_frame_url = omni_request.end_frame_image_media_token.as_ref()
|
||||
.and_then(|t| url_map.get(t).cloned());
|
||||
|
||||
let reference_image_urls = omni_request.reference_image_media_tokens.as_ref()
|
||||
.map(|tokens| tokens.iter().filter_map(|t| url_map.get(t).cloned()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let reference_video_urls = omni_request.reference_video_media_tokens.as_ref()
|
||||
.map(|tokens| tokens.iter().filter_map(|t| url_map.get(t).cloned()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let reference_audio_urls = omni_request.reference_audio_media_tokens.as_ref()
|
||||
.map(|tokens| tokens.iter().filter_map(|t| url_map.get(t).cloned()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(ResolvedVideoMedia {
|
||||
url_map,
|
||||
maybe_start_frame_url,
|
||||
maybe_end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply resolved media URLs to a GenerateVideoRequest, replacing MediaFileToken refs with URL refs.
|
||||
pub fn apply_resolved_media<'a>(
|
||||
request: &mut GenerateVideoRequest<'a>,
|
||||
resolved: &'a ResolvedVideoMedia,
|
||||
) {
|
||||
request.start_frame = resolved.maybe_start_frame_url.as_deref()
|
||||
.map(ImageRef::Url);
|
||||
|
||||
request.end_frame = resolved.maybe_end_frame_url.as_deref()
|
||||
.map(ImageRef::Url);
|
||||
|
||||
if !resolved.reference_image_urls.is_empty() {
|
||||
request.reference_images = Some(ImageListRef::Urls(&resolved.reference_image_urls));
|
||||
} else {
|
||||
request.reference_images = None;
|
||||
}
|
||||
|
||||
if !resolved.reference_video_urls.is_empty() {
|
||||
request.reference_videos = Some(VideoListRef::Urls(&resolved.reference_video_urls));
|
||||
} else {
|
||||
request.reference_videos = None;
|
||||
}
|
||||
|
||||
if !resolved.reference_audio_urls.is_empty() {
|
||||
request.reference_audio = Some(AudioListRef::Urls(&resolved.reference_audio_urls));
|
||||
} else {
|
||||
request.reference_audio = None;
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_router::api::audio_list_ref::AudioListRef;
|
||||
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioRouter;
|
||||
use artcraft_router::api::common_resolution::CommonResolution as CommonResolutionRouter;
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel as CommonVideoModelRouter;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::api::video_list_ref::VideoListRef;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use artcraft_router::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
fn convert_model(
|
||||
model: &CommonVideoModelEnum,
|
||||
) -> Result<CommonVideoModelRouter, AdvancedCommonWebError> {
|
||||
let json = serde_json::to_string(model)?;
|
||||
serde_json::from_str(&json).map_err(|e| {
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
format!("Unsupported video model: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_aspect_ratio(
|
||||
ar: &CommonAspectRatioEnum,
|
||||
) -> Result<CommonAspectRatioRouter, AdvancedCommonWebError> {
|
||||
let json = serde_json::to_string(ar)?;
|
||||
serde_json::from_str(&json).map_err(|e| {
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
format!("Unsupported aspect ratio: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_resolution(
|
||||
res: &CommonResolutionEnum,
|
||||
) -> Result<CommonResolutionRouter, AdvancedCommonWebError> {
|
||||
let json = serde_json::to_string(res)?;
|
||||
serde_json::from_str(&json).map_err(|e| {
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
format!("Unsupported resolution: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn transform_request(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
) -> Result<GenerateVideoRequest<'_>, AdvancedCommonWebError> {
|
||||
let api_model = request.model.as_ref()
|
||||
.ok_or_else(|| AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
"model is required".to_string(),
|
||||
))?;
|
||||
|
||||
let model = convert_model(api_model)?;
|
||||
|
||||
let aspect_ratio = request.aspect_ratio.as_ref()
|
||||
.map(convert_aspect_ratio)
|
||||
.transpose()?;
|
||||
|
||||
let resolution = request.resolution.as_ref()
|
||||
.map(convert_resolution)
|
||||
.transpose()?;
|
||||
|
||||
Ok(GenerateVideoRequest {
|
||||
model,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: request.prompt.as_deref(),
|
||||
negative_prompt: request.negative_prompt.as_deref(),
|
||||
start_frame: request.start_frame_image_media_token.as_ref()
|
||||
.map(ImageRef::MediaFileToken),
|
||||
end_frame: request.end_frame_image_media_token.as_ref()
|
||||
.map(ImageRef::MediaFileToken),
|
||||
reference_images: request.reference_image_media_tokens.as_ref()
|
||||
.map(ImageListRef::MediaFileTokens),
|
||||
reference_videos: request.reference_video_media_tokens.as_ref()
|
||||
.map(VideoListRef::MediaFileTokens),
|
||||
reference_audio: request.reference_audio_media_tokens.as_ref()
|
||||
.map(AudioListRef::MediaFileTokens),
|
||||
reference_character_tokens: None,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
duration_seconds: request.duration_seconds,
|
||||
video_batch_count: request.video_batch_count,
|
||||
generate_audio: request.generate_audio,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
idempotency_token: request.idempotency_token.as_deref(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user