mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 17:28:24 -04:00
Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b747c27b2 | ||
|
|
d5fa61ec3c | ||
|
|
c84dab45ca | ||
|
|
45651103f0 | ||
|
|
cba8e2c074 | ||
|
|
07d9d537dd | ||
|
|
5770534fc3 | ||
|
|
6fe0909879 | ||
|
|
834f0b48a9 | ||
|
|
d64944b1c3 | ||
|
|
03edd5faf6 | ||
|
|
0ef57ff97e | ||
|
|
adda7483e8 | ||
|
|
e77b946d57 | ||
|
|
3a6fad1812 | ||
|
|
9e04990224 | ||
|
|
885b68630a | ||
|
|
9fee0b1c99 | ||
|
|
631b98f605 | ||
|
|
3334cf7b81 | ||
|
|
50d8abcfb1 | ||
|
|
bce8d166e0 | ||
|
|
5fc27a5153 | ||
|
|
51e5db6b12 | ||
|
|
ff1ce82f66 | ||
|
|
ccea741443 | ||
|
|
2b9882d2d7 | ||
|
|
55cab30dec | ||
|
|
a2f204b1be | ||
|
|
12e4ff7a29 | ||
|
|
8b73569238 | ||
|
|
badabe57eb | ||
|
|
2e62eef85b | ||
|
|
fd9f002615 | ||
|
|
a0e50067bb |
Generated
+1
-1
@@ -6089,7 +6089,6 @@ dependencies = [
|
||||
name = "jwt_signer"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"errors",
|
||||
"hmac 0.10.1",
|
||||
"jwt",
|
||||
"sha2 0.9.9",
|
||||
@@ -10173,6 +10172,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bootstrap",
|
||||
"bucket_paths",
|
||||
"chrono",
|
||||
"cloud_storage",
|
||||
"concurrency",
|
||||
"easyenv",
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ pub struct GenerateKling16ProImageToVideoRequest {
|
||||
pub duration: Option<GenerateKling16ProDuration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling16ProAspectRatio {
|
||||
/// 16:9 aspect ratio
|
||||
@@ -39,7 +39,7 @@ pub enum GenerateKling16ProAspectRatio {
|
||||
Square,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling16ProDuration {
|
||||
FiveSeconds,
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ pub struct GenerateKling21MasterImageToVideoRequest {
|
||||
pub duration: Option<GenerateKling21MasterDuration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling21MasterAspectRatio {
|
||||
/// 16:9 aspect ratio
|
||||
@@ -35,7 +35,7 @@ pub enum GenerateKling21MasterAspectRatio {
|
||||
Square,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling21MasterDuration {
|
||||
FiveSeconds,
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ pub struct GenerateKling21ProImageToVideoRequest {
|
||||
pub duration: Option<GenerateKling21ProDuration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling21ProAspectRatio {
|
||||
/// 16:9 aspect ratio
|
||||
@@ -39,7 +39,7 @@ pub enum GenerateKling21ProAspectRatio {
|
||||
Square,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateKling21ProDuration {
|
||||
FiveSeconds,
|
||||
|
||||
+3
-3
@@ -31,14 +31,14 @@ pub struct GenerateSeedance10LiteImageToVideoRequest {
|
||||
pub aspect_ratio: Option<GenerateSeedance10LiteAspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateSeedance10LiteResolution {
|
||||
FourEightyP,
|
||||
SevenTwentyP,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateSeedance10LiteDuration {
|
||||
FiveSeconds,
|
||||
@@ -46,7 +46,7 @@ pub enum GenerateSeedance10LiteDuration {
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateSeedance10LiteAspectRatio {
|
||||
Auto,
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ pub struct GenerateVeo2ImageToVideoRequest {
|
||||
pub duration: Option<GenerateVeo2Duration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo2AspectRatio {
|
||||
Auto,
|
||||
@@ -33,7 +33,7 @@ pub enum GenerateVeo2AspectRatio {
|
||||
TallNineSixteen, // 9:16
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo2Duration {
|
||||
FiveSeconds,
|
||||
|
||||
+2
-2
@@ -27,14 +27,14 @@ pub struct GenerateVeo3FastImageToVideoRequest {
|
||||
pub generate_audio: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3FastResolution {
|
||||
SevenTwentyP, // 720p
|
||||
TenEightyP, // 1080p
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3FastDuration {
|
||||
EightSeconds,
|
||||
|
||||
+3
-3
@@ -37,7 +37,7 @@ pub struct GenerateVeo3ImageToVideoRequest {
|
||||
// pub auto_fix: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3AspectRatio {
|
||||
WideSixteenNine, // 16:9
|
||||
@@ -45,14 +45,14 @@ pub enum GenerateVeo3AspectRatio {
|
||||
Square, // 1:1
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3Resolution {
|
||||
SevenTwentyP, // 720p
|
||||
TenEightyP, // 1080p
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3Duration {
|
||||
EightSeconds,
|
||||
|
||||
+2
-2
@@ -41,14 +41,14 @@ pub struct Kling2p5TurboProMultiFunctionVideoGenRequest {
|
||||
pub aspect_ratio: Option<Kling2p5TurboProMultiFunctionVideoGenAspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Kling2p5TurboProMultiFunctionVideoGenDuration {
|
||||
FiveSeconds,
|
||||
TenSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Kling2p5TurboProMultiFunctionVideoGenAspectRatio {
|
||||
Square,
|
||||
|
||||
+2
-2
@@ -45,14 +45,14 @@ pub struct Kling2p6ProMultiFunctionVideoGenRequest {
|
||||
pub aspect_ratio: Option<Kling2p6ProMultiFunctionVideoGenAspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Kling2p6ProMultiFunctionVideoGenDuration {
|
||||
FiveSeconds,
|
||||
TenSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Kling2p6ProMultiFunctionVideoGenAspectRatio {
|
||||
Square,
|
||||
|
||||
+3
-3
@@ -35,14 +35,14 @@ pub struct Sora2MultiFunctionVideoGenRequest {
|
||||
pub aspect_ratio: Option<Sora2MultiFunctionVideoGenAspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Sora2MultiFunctionVideoGenResolution {
|
||||
Auto,
|
||||
SevenTwentyP,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Sora2MultiFunctionVideoGenDuration {
|
||||
FourSeconds,
|
||||
@@ -50,7 +50,7 @@ pub enum Sora2MultiFunctionVideoGenDuration {
|
||||
TwelveSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Sora2MultiFunctionVideoGenAspectRatio {
|
||||
Auto,
|
||||
|
||||
+3
-3
@@ -35,7 +35,7 @@ pub struct Sora2ProMultiFunctionVideoGenRequest {
|
||||
pub aspect_ratio: Option<Sora2ProMultiFunctionVideoGenAspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Sora2ProMultiFunctionVideoGenResolution {
|
||||
Auto,
|
||||
@@ -43,7 +43,7 @@ pub enum Sora2ProMultiFunctionVideoGenResolution {
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Sora2ProMultiFunctionVideoGenDuration {
|
||||
FourSeconds,
|
||||
@@ -51,7 +51,7 @@ pub enum Sora2ProMultiFunctionVideoGenDuration {
|
||||
TwelveSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Sora2ProMultiFunctionVideoGenAspectRatio {
|
||||
Auto,
|
||||
|
||||
+3
-3
@@ -60,7 +60,7 @@ pub struct Veo3p1FastMultiFunctionVideoGenRequest {
|
||||
pub auto_fix: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Veo3p1FastMultiFunctionVideoGenDuration {
|
||||
FourSeconds,
|
||||
@@ -68,7 +68,7 @@ pub enum Veo3p1FastMultiFunctionVideoGenDuration {
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Veo3p1FastMultiFunctionVideoGenAspectRatio {
|
||||
Auto,
|
||||
@@ -76,7 +76,7 @@ pub enum Veo3p1FastMultiFunctionVideoGenAspectRatio {
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Veo3p1FastMultiFunctionVideoGenResolution {
|
||||
SevenTwentyP,
|
||||
|
||||
+3
-3
@@ -60,7 +60,7 @@ pub struct Veo3p1MultiFunctionVideoGenRequest {
|
||||
pub auto_fix: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Veo3p1MultiFunctionVideoGenDuration {
|
||||
FourSeconds,
|
||||
@@ -68,7 +68,7 @@ pub enum Veo3p1MultiFunctionVideoGenDuration {
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Veo3p1MultiFunctionVideoGenAspectRatio {
|
||||
Auto,
|
||||
@@ -76,7 +76,7 @@ pub enum Veo3p1MultiFunctionVideoGenAspectRatio {
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Veo3p1MultiFunctionVideoGenResolution {
|
||||
SevenTwentyP,
|
||||
|
||||
+3
-3
@@ -40,11 +40,11 @@ pub struct OmniGenImageCostAndGenerateRequest {
|
||||
pub image_batch_count: Option<u16>,
|
||||
|
||||
/// Only for angle manipulation models.
|
||||
pub horizontal_angle: Option<f64>,
|
||||
pub adjust_horizontal_angle: Option<f64>,
|
||||
|
||||
/// Only for angle manipulation models.
|
||||
pub vertical_angle: Option<f64>,
|
||||
pub adjust_vertical_angle: Option<f64>,
|
||||
|
||||
/// Only for angle manipulation models.
|
||||
pub zoom: Option<f64>,
|
||||
pub adjust_zoom: Option<f64>,
|
||||
}
|
||||
|
||||
+5
@@ -5,6 +5,7 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_quality::CommonQuality;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Shared request body for both the video cost estimate and video generation endpoints.
|
||||
@@ -39,6 +40,10 @@ pub struct OmniGenVideoCostAndGenerateRequest {
|
||||
/// Reference audio (optional).
|
||||
pub reference_audio_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Optional character tokens to reference in the prompt.
|
||||
/// Characters are referenced in prompts as @CharacterName.
|
||||
pub reference_character_tokens: Option<Vec<CharacterToken>>,
|
||||
|
||||
/// The resolution to use.
|
||||
pub resolution: Option<CommonResolution>,
|
||||
|
||||
|
||||
+1
@@ -103,6 +103,7 @@ pub struct OmniGenImageModelDetails {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution_default: Option<CommonResolution>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quality_options: Option<Vec<CommonQuality>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
+14
-3
@@ -1,10 +1,13 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_quality::CommonQuality;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
/// Video model to default to if none is specified
|
||||
const DEFAULT_VIDEO_MODEL : CommonVideoModel = CommonVideoModel::Seedance2p0;
|
||||
|
||||
/// Query string parameters for the video models endpoint.
|
||||
#[derive(Deserialize, IntoParams, ToSchema)]
|
||||
@@ -110,6 +113,12 @@ pub struct OmniGenVideoModelDetails {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub audio_references_max_total_duration_seconds: Option<u16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub character_references_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub character_references_max: Option<u16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub show_generate_with_sound_toggle: Option<bool>,
|
||||
|
||||
@@ -159,7 +168,7 @@ pub struct OmniGenVideoModelDetails {
|
||||
impl Default for OmniGenVideoModelDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
model: DEFAULT_VIDEO_MODEL,
|
||||
full_name: None,
|
||||
text_prompt_supported: None,
|
||||
text_prompt_max_length: None,
|
||||
@@ -176,6 +185,8 @@ impl Default for OmniGenVideoModelDetails {
|
||||
audio_references_supported: None,
|
||||
audio_references_max: None,
|
||||
audio_references_max_total_duration_seconds: None,
|
||||
character_references_supported: None,
|
||||
character_references_max: None,
|
||||
show_generate_with_sound_toggle: None,
|
||||
aspect_ratio_options: None,
|
||||
aspect_ratio_default: None,
|
||||
|
||||
@@ -36,7 +36,11 @@ pub enum CommonAspectRatio {
|
||||
|
||||
// Auto values that bake in resolution
|
||||
// These are from the Seedream models
|
||||
#[serde(rename = "auto_2k")]
|
||||
Auto2k,
|
||||
#[serde(rename = "auto_3k")]
|
||||
Auto3k,
|
||||
#[serde(rename = "auto_4k")]
|
||||
Auto4k,
|
||||
|
||||
// Defined aspect ratios that bake in resolution
|
||||
|
||||
@@ -13,6 +13,8 @@ pub enum CommonImageModel {
|
||||
FluxPro11,
|
||||
#[serde(rename = "flux_pro_1p1_ultra")]
|
||||
FluxPro11Ultra,
|
||||
#[serde(rename = "gpt_image_1")]
|
||||
GptImage1,
|
||||
#[serde(rename = "gpt_image_1p5")]
|
||||
GptImage1p5,
|
||||
#[serde(rename = "nano_banana")]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Common quality levels you can specify when enqueuing a generation.
|
||||
/// Not every model will use this; models that don't simply ignore it.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonQuality {
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
}
|
||||
@@ -2,6 +2,7 @@ pub mod audio_list_ref;
|
||||
pub mod character_list_ref;
|
||||
pub mod common_aspect_ratio;
|
||||
pub mod common_image_model;
|
||||
pub mod common_quality;
|
||||
pub mod common_resolution;
|
||||
pub mod common_splat_model;
|
||||
pub mod common_video_model;
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -46,6 +46,7 @@ mod tests {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
|
||||
+11
-7
@@ -6,8 +6,11 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
pub(crate) fn estimate_image_cost_artcraft_flux_pro_1p1(
|
||||
plan: &PlanArtcraftFluxPro11<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// NB(bt): Slightly more than 3 cents, weird megapixel maths. We can eat that cost for now.
|
||||
let cost_per_image: u64 = 3;
|
||||
// Fal pricing: $0.04 per megapixel, billed by rounding up to the nearest
|
||||
// megapixel. The default image_size values are ~1MP, so 4 cents per image.
|
||||
// (The legacy `generate_flux_pro_11_text_to_image_handler` charges this same
|
||||
// amount via `FluxPro11Args::calculate_cost_in_cents`.)
|
||||
let cost_per_image: u64 = 4;
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
GenerateFluxPro11TextToImageNumImages::One => 1,
|
||||
@@ -43,6 +46,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
@@ -60,10 +64,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_usd_cents() {
|
||||
// ~$0.03/image = 3 cents each (slightly more than 3¢ actual, rounded up)
|
||||
assert_eq!(estimate_usd_cents(1), 3);
|
||||
assert_eq!(estimate_usd_cents(2), 6);
|
||||
assert_eq!(estimate_usd_cents(3), 9);
|
||||
assert_eq!(estimate_usd_cents(4), 12);
|
||||
// $0.04/image = 4 cents each (Fal's $0.04/MP at the default ~1MP).
|
||||
assert_eq!(estimate_usd_cents(1), 4);
|
||||
assert_eq!(estimate_usd_cents(2), 8);
|
||||
assert_eq!(estimate_usd_cents(3), 12);
|
||||
assert_eq!(estimate_usd_cents(4), 16);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1::{
|
||||
ArtcraftGptImage1Quality, ArtcraftGptImage1Size, PlanArtcraftGptImage1,
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_gpt_image_1(
|
||||
plan: &PlanArtcraftGptImage1<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing per the fal-ai GPT Image 1 docs, adapted for our artcraft billing:
|
||||
//
|
||||
// Output image cost (per output image):
|
||||
// Low – $0.011 (1024×1024) / $0.016 (other) → 2¢ / 2¢
|
||||
// Medium – $0.042 (1024×1024) / $0.063 (other) → 5¢ / 7¢
|
||||
// High – $0.167 (1024×1024) / $0.250 (other) → 17¢ / 25¢
|
||||
//
|
||||
// Input image cost (edit mode only, per input image, high-fidelity assumed):
|
||||
// 3,050 tokens × $0.005/1K tokens ≈ $0.01525 → 2¢ per input image
|
||||
//
|
||||
// Input text tokens: free (we don't charge for these).
|
||||
//
|
||||
let is_square = matches!(
|
||||
plan.image_size,
|
||||
None | Some(ArtcraftGptImage1Size::Square)
|
||||
);
|
||||
|
||||
let output_cost_per_image: u64 = match (plan.quality, is_square) {
|
||||
(ArtcraftGptImage1Quality::Auto, _) => 17, // Auto treated as High square
|
||||
(ArtcraftGptImage1Quality::Low, true) => 2,
|
||||
(ArtcraftGptImage1Quality::Low, false) => 2,
|
||||
(ArtcraftGptImage1Quality::Medium, true) => 5,
|
||||
(ArtcraftGptImage1Quality::Medium, false) => 7,
|
||||
(ArtcraftGptImage1Quality::High, true) => 17,
|
||||
(ArtcraftGptImage1Quality::High, false) => 25,
|
||||
};
|
||||
|
||||
let num_output_images = plan.num_images.as_u64();
|
||||
let output_cost = output_cost_per_image * num_output_images;
|
||||
|
||||
// Input image token cost: 2¢ per input image (high-fidelity estimate).
|
||||
let input_image_cost: u64 = 2 * plan.num_input_images;
|
||||
|
||||
let cost_in_usd_cents = output_cost + input_image_cost;
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn base() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::GptImage1,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_text(
|
||||
quality: Option<CommonQuality>,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
batch: u16,
|
||||
) -> u64 {
|
||||
let request = GenerateImageRequest {
|
||||
quality,
|
||||
aspect_ratio,
|
||||
image_batch_count: Some(batch),
|
||||
..base()
|
||||
};
|
||||
request.build().unwrap().estimate_costs()
|
||||
.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
fn estimate_edit(
|
||||
quality: Option<CommonQuality>,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
batch: u16,
|
||||
num_input_images: usize,
|
||||
) -> u64 {
|
||||
let tokens: Vec<MediaFileToken> = (0..num_input_images)
|
||||
.map(|i| MediaFileToken::new_from_str(&format!("mf_test{:028}", i)))
|
||||
.collect();
|
||||
let request = GenerateImageRequest {
|
||||
quality,
|
||||
aspect_ratio,
|
||||
image_batch_count: Some(batch),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base()
|
||||
};
|
||||
request.build().unwrap().estimate_costs()
|
||||
.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// ── Default quality (None → High) ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_quality_square_one_image_costs_17_cents() {
|
||||
assert_eq!(estimate_text(None, None, 1), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_quality_square_four_images_costs_68_cents() {
|
||||
assert_eq!(estimate_text(None, None, 4), 68);
|
||||
}
|
||||
|
||||
// ── Low quality (2¢/image regardless of size) ───────────────────────────
|
||||
|
||||
#[test]
|
||||
fn low_square_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), 1), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_wide_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), 1), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_tall_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::TallNineBySixteen), 1), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_four_images() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Low), None, 4), 8);
|
||||
}
|
||||
|
||||
// ── Medium quality (5¢ square, 7¢ wide/tall) ───────────────────────────
|
||||
|
||||
#[test]
|
||||
fn medium_square_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 1), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_wide_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 1), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_tall_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 1), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_square_four_images() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 4), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_wide_four_images() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 4), 28);
|
||||
}
|
||||
|
||||
// ── High quality (17¢ square, 25¢ wide/tall) ───────────────────────────
|
||||
|
||||
#[test]
|
||||
fn high_square_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wide_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 1), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_tall_one_image() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), 1), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_four_images() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 4), 68);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wide_four_images() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 4), 100);
|
||||
}
|
||||
|
||||
// ── Edit mode: adds 2¢ per input image ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn edit_one_input_adds_2_cents() {
|
||||
// High square 1 output = 17¢, plus 1 input × 2¢ = 19¢
|
||||
assert_eq!(estimate_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1, 1), 19);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_three_inputs_adds_6_cents() {
|
||||
// High square 1 output = 17¢, plus 3 inputs × 2¢ = 23¢
|
||||
assert_eq!(estimate_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1, 3), 23);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_five_inputs_adds_10_cents() {
|
||||
// Medium wide 2 outputs = 7×2 = 14¢, plus 5 inputs × 2¢ = 24¢
|
||||
assert_eq!(estimate_edit(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 2, 5), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_low_quality_with_inputs() {
|
||||
// Low square 1 output = 2¢, plus 2 inputs × 2¢ = 6¢
|
||||
assert_eq!(estimate_edit(Some(CommonQuality::Low), None, 1, 2), 6);
|
||||
}
|
||||
|
||||
// ── Text mode (no input images) has no input cost ──────────────────────
|
||||
|
||||
#[test]
|
||||
fn text_mode_no_input_cost() {
|
||||
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 17);
|
||||
}
|
||||
}
|
||||
+157
-54
@@ -9,42 +9,33 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
pub(crate) fn estimate_image_cost_artcraft_gpt_image_1p5(
|
||||
plan: &PlanArtcraftGptImage1p5<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing varies by quality and image size. 1 credit = 1 USD cent.
|
||||
// Low: $0.01/image (any size)
|
||||
// Medium: $0.03/image (square/unset), $0.05/image (wide or tall)
|
||||
// High: $0.13/image (square/unset), $0.20/image (wide or tall)
|
||||
// Per fal docs (fal-ai/gpt-image-1.5 and fal-ai/gpt-image-1.5/edit):
|
||||
//
|
||||
// From Fal (text to image) -
|
||||
// Output image cost (per output image), rounded up to whole cents:
|
||||
// Low: $0.009 (1024×1024) / $0.013 (other) → 1¢ / 2¢
|
||||
// Medium: $0.034 (1024×1024) / $0.050 (1536×1024) / $0.051 (1024×1536) → 4¢ / 5¢ / 6¢
|
||||
// High: $0.133 (1024×1024) / $0.199 (1536×1024) / $0.200 (1024×1536) → 14¢ / 20¢ / 20¢
|
||||
//
|
||||
// Your request will cost different amounts based on the number of images, quality, and size.
|
||||
// Input text tokens: free (we don't charge for these).
|
||||
//
|
||||
// You will be charged $0.005 per 1,000 input text tokens. One word is roughly 4 tokens.
|
||||
// You will be charged $0.010 per 1,000 output text tokens. The model will consume tokens reasoning about your prompt based on it's complexity.
|
||||
// For low quality, you will be charged $0.009 for 1024x1024 or $0.013 for any other size per image.
|
||||
// For medium quality, you will be charged $0.034 for 1024x1024, $0.051 for 1024x1536 and $0.050 for 1536x1024 per image.
|
||||
// For high quality, you will be charged $0.133 for 1024x1024, $0.200 for 1024x1536 or $0.199 for 1536x1024 per image.
|
||||
// Default quality: High (when request.quality is None).
|
||||
//
|
||||
// From Fal (image to image) -
|
||||
//
|
||||
// Your request will cost different amounts based on the number of images, quality, and size.
|
||||
//
|
||||
// You will be charged $0.005 per 1,000 input text tokens. One word is roughly 4 tokens.
|
||||
// You will be charged $0.008 per 1,000 input image tokens. One 1024x1024 image is roughly 135 tokens in low fidelity mode, or 3,050 tokens in high fidelity mode.
|
||||
// You will be charged $0.010 per 1,000 output text tokens. The model will consume tokens reasoning about your prompt based on it's complexity.
|
||||
// For low quality, you will be charged $0.009 for 1024x1024 or $0.013 for any other size per image.
|
||||
// For medium quality, you will be charged $0.034 for 1024x1024, $0.051 for 1024x1536 and $0.050 for 1536x1024 per image.
|
||||
// For high quality, you will be charged $0.133 for 1024x1024, $0.200 for 1024x1536 or $0.199 for 1536x1024 per image.
|
||||
//
|
||||
let cost_per_image: u64 = match plan.quality {
|
||||
GptImage1p5MultiFunctionImageGenQuality::Low => 1,
|
||||
GptImage1p5MultiFunctionImageGenQuality::Medium => match plan.image_size {
|
||||
Some(GptImage1p5MultiFunctionImageGenSize::Square) | None => 3,
|
||||
Some(GptImage1p5MultiFunctionImageGenSize::Wide) | Some(GptImage1p5MultiFunctionImageGenSize::Tall) => 5,
|
||||
},
|
||||
GptImage1p5MultiFunctionImageGenQuality::High => match plan.image_size {
|
||||
Some(GptImage1p5MultiFunctionImageGenSize::Square) | None => 13,
|
||||
Some(GptImage1p5MultiFunctionImageGenSize::Wide) | Some(GptImage1p5MultiFunctionImageGenSize::Tall) => 20,
|
||||
},
|
||||
use GptImage1p5MultiFunctionImageGenQuality as Q;
|
||||
use GptImage1p5MultiFunctionImageGenSize as S;
|
||||
|
||||
let cost_per_image: u64 = match (plan.quality, plan.image_size) {
|
||||
// Low quality
|
||||
(Q::Low, None | Some(S::Square)) => 1,
|
||||
(Q::Low, Some(S::Wide) | Some(S::Tall)) => 2,
|
||||
|
||||
// Medium quality
|
||||
(Q::Medium, None | Some(S::Square)) => 4,
|
||||
(Q::Medium, Some(S::Wide)) => 5,
|
||||
(Q::Medium, Some(S::Tall)) => 6,
|
||||
|
||||
// High quality
|
||||
(Q::High, None | Some(S::Square)) => 14,
|
||||
(Q::High, Some(S::Wide) | Some(S::Tall)) => 20,
|
||||
};
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
@@ -70,11 +61,16 @@ pub(crate) fn estimate_image_cost_artcraft_gpt_image_1p5(
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
|
||||
fn estimate_usd_cents(image_batch_count: u16, aspect_ratio: Option<CommonAspectRatio>) -> u64 {
|
||||
fn estimate(
|
||||
quality: Option<CommonQuality>,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
batch: u16,
|
||||
) -> u64 {
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::GptImage1p5,
|
||||
provider: Provider::Artcraft,
|
||||
@@ -82,7 +78,8 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
quality,
|
||||
image_batch_count: Some(batch),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
@@ -90,34 +87,140 @@ mod tests {
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
request.build().unwrap().estimate_costs()
|
||||
.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// ── Default quality (None → High) ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_quality_square_one_image() {
|
||||
assert_eq!(estimate(None, Some(CommonAspectRatio::Square), 1), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_medium_square() {
|
||||
// Default quality (Medium) + square/unset size = 3 cents each
|
||||
assert_eq!(estimate_usd_cents(1, None), 3);
|
||||
assert_eq!(estimate_usd_cents(1, Some(CommonAspectRatio::Square)), 3);
|
||||
assert_eq!(estimate_usd_cents(2, Some(CommonAspectRatio::Square)), 6);
|
||||
assert_eq!(estimate_usd_cents(3, Some(CommonAspectRatio::Square)), 9);
|
||||
assert_eq!(estimate_usd_cents(4, Some(CommonAspectRatio::Square)), 12);
|
||||
fn default_quality_unset_one_image() {
|
||||
assert_eq!(estimate(None, None, 1), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_medium_wide() {
|
||||
// Medium + wide = 5 cents each
|
||||
assert_eq!(estimate_usd_cents(1, Some(CommonAspectRatio::WideSixteenByNine)), 5);
|
||||
assert_eq!(estimate_usd_cents(4, Some(CommonAspectRatio::WideSixteenByNine)), 20);
|
||||
fn default_quality_wide_one_image() {
|
||||
assert_eq!(estimate(None, Some(CommonAspectRatio::WideSixteenByNine), 1), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_medium_tall() {
|
||||
// Medium + tall = 5 cents each
|
||||
assert_eq!(estimate_usd_cents(1, Some(CommonAspectRatio::TallNineBySixteen)), 5);
|
||||
assert_eq!(estimate_usd_cents(4, Some(CommonAspectRatio::TallNineBySixteen)), 20);
|
||||
fn default_quality_tall_one_image() {
|
||||
assert_eq!(estimate(None, Some(CommonAspectRatio::TallNineBySixteen), 1), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_quality_square_four_images() {
|
||||
assert_eq!(estimate(None, Some(CommonAspectRatio::Square), 4), 56);
|
||||
}
|
||||
|
||||
// ── Low quality (1¢ square, 2¢ wide/tall) ─────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn low_square_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), 1), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_unset_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Low), None, 1), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_wide_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), 1), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_tall_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Low), Some(CommonAspectRatio::TallNineBySixteen), 1), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_square_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Low), None, 4), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_wide_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), 4), 8);
|
||||
}
|
||||
|
||||
// ── Medium quality (4¢ square, 5¢ wide, 6¢ tall) ─────────────────────
|
||||
|
||||
#[test]
|
||||
fn medium_square_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 1), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_unset_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), None, 1), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_wide_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 1), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_tall_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 1), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_square_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 4), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_wide_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 4), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_tall_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 4), 24);
|
||||
}
|
||||
|
||||
// ── High quality (14¢ square, 20¢ wide/tall) ─────────────────────────
|
||||
|
||||
#[test]
|
||||
fn high_square_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_unset_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), None, 1), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wide_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 1), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_tall_one() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), 1), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 4), 56);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wide_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 4), 80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_tall_four() {
|
||||
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), 4), 80);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+130
-16
@@ -8,12 +8,17 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
pub(crate) fn estimate_image_cost_artcraft_nano_banana_2(
|
||||
plan: &PlanArtcraftNanaBanana2<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.08/image at 0.5K, $0.15/image at 1K or 2K, $0.30/image at 4K.
|
||||
// (Stored as USD cents.)
|
||||
// Pricing (USD cents per image), scaled off the 1K base cost of $0.08:
|
||||
// 0.5K → $0.06 (0.75×)
|
||||
// 1K → $0.08 (1×, default)
|
||||
// 2K → $0.12 (1.5×)
|
||||
// 4K → $0.16 (2×)
|
||||
let cost_per_image: u64 = match plan.resolution {
|
||||
Some(NanaBanana2MultiFunctionImageGenImageResolution::HalfK) => 8,
|
||||
Some(NanaBanana2MultiFunctionImageGenImageResolution::FourK) => 30,
|
||||
_ => 15, // OneK, TwoK, or None (default 1K)
|
||||
Some(NanaBanana2MultiFunctionImageGenImageResolution::HalfK) => 6,
|
||||
Some(NanaBanana2MultiFunctionImageGenImageResolution::OneK) => 8,
|
||||
Some(NanaBanana2MultiFunctionImageGenImageResolution::TwoK) => 12,
|
||||
Some(NanaBanana2MultiFunctionImageGenImageResolution::FourK) => 16,
|
||||
None => 8, // default 1K
|
||||
};
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
@@ -51,6 +56,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
@@ -66,17 +72,125 @@ mod tests {
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_usd_cents() {
|
||||
// 1K/2K: $0.15/image = 15 cents
|
||||
assert_eq!(estimate_usd_cents(None, 1), 15);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 1), 15);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 1), 15);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 2), 30);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 4), 60);
|
||||
// ── Default (None → 1K → 8¢/image) ─────────────────────────────────────────
|
||||
|
||||
// 4K: $0.30/image = 30 cents
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 1), 30);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 2), 60);
|
||||
#[test]
|
||||
fn default_resolution_one_image_costs_8_cents() {
|
||||
assert_eq!(estimate_usd_cents(None, 1), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_resolution_two_images_costs_16_cents() {
|
||||
assert_eq!(estimate_usd_cents(None, 2), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_resolution_three_images_costs_24_cents() {
|
||||
assert_eq!(estimate_usd_cents(None, 3), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_resolution_four_images_costs_32_cents() {
|
||||
assert_eq!(estimate_usd_cents(None, 4), 32);
|
||||
}
|
||||
|
||||
// ── 0.5K → 6¢/image (0.75× base) ───────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn half_k_one_image_costs_6_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::HalfK), 1), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn half_k_two_images_costs_12_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::HalfK), 2), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn half_k_three_images_costs_18_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::HalfK), 3), 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn half_k_four_images_costs_24_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::HalfK), 4), 24);
|
||||
}
|
||||
|
||||
// ── 1K → 8¢/image (base) ───────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn one_k_one_image_costs_8_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 1), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_k_two_images_costs_16_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 2), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_k_three_images_costs_24_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 3), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_k_four_images_costs_32_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 4), 32);
|
||||
}
|
||||
|
||||
// ── 2K → 12¢/image (1.5× base) ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn two_k_one_image_costs_12_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 1), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_k_two_images_costs_24_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 2), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_k_three_images_costs_36_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 3), 36);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_k_four_images_costs_48_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 4), 48);
|
||||
}
|
||||
|
||||
// ── 4K → 16¢/image (2× base) ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn four_k_one_image_costs_16_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 1), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_two_images_costs_32_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 2), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_three_images_costs_48_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 3), 48);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_four_images_costs_64_cents() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 4), 64);
|
||||
}
|
||||
|
||||
// ── 3K (unsupported) falls back to 2K pricing ──────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn three_k_one_image_falls_back_to_two_k_pricing() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::ThreeK), 1), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_k_four_images_falls_back_to_two_k_pricing() {
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::ThreeK), 4), 48);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -50,6 +50,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -46,6 +46,7 @@ mod tests {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+1
@@ -2,6 +2,7 @@ pub mod estimate_image_cost_artcraft_flux_1_dev;
|
||||
pub mod estimate_image_cost_artcraft_flux_1_schnell;
|
||||
pub mod estimate_image_cost_artcraft_flux_pro_1p1;
|
||||
pub mod estimate_image_cost_artcraft_flux_pro_1p1_ultra;
|
||||
pub mod estimate_image_cost_artcraft_gpt_image_1;
|
||||
pub mod estimate_image_cost_artcraft_gpt_image_1p5;
|
||||
pub mod estimate_image_cost_artcraft_nano_banana;
|
||||
pub mod estimate_image_cost_artcraft_nano_banana_2;
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1::PlanFalFluxPro11;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_pro_1p1(
|
||||
plan: &PlanFalFluxPro11<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04 per megapixel, billed by rounding up to nearest megapixel.
|
||||
// Default image sizes are ~1MP (1024x1024), so ~$0.04/image = 4 cents.
|
||||
let cost_per_image: u64 = 4;
|
||||
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,
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::PlanFalFluxPro11Ultra;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_pro_1p1_ultra(
|
||||
plan: &PlanFalFluxPro11Ultra<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.06 per image.
|
||||
let cost_per_image: u64 = 6;
|
||||
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,
|
||||
}
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::{
|
||||
FalGptImage1ImageSize, FalGptImage1Quality, PlanFalGptImage1,
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_gpt_image_1(
|
||||
plan: &PlanFalGptImage1<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Per fal docs (fal-ai/gpt-image-1/{text-to-image,edit-image}):
|
||||
//
|
||||
// Output image cost (per output image):
|
||||
// Low: $0.011 (1024×1024) / $0.016 (other) → 2¢ / 2¢
|
||||
// Medium: $0.042 (1024×1024) / $0.063 (other) → 5¢ / 7¢
|
||||
// High: $0.167 (1024×1024) / $0.250 (other) → 17¢ / 25¢
|
||||
//
|
||||
// Input image tokens (edit mode, per input image, high-fidelity assumed):
|
||||
// 3,050 tokens × $0.005/1K tokens ≈ $0.01525 → 2¢ per input image
|
||||
//
|
||||
// Input text tokens: free (we don't charge for these).
|
||||
//
|
||||
let is_square = matches!(plan.image_size, None | Some(FalGptImage1ImageSize::Square));
|
||||
let output_cost_per_image: u64 = match (plan.quality, is_square) {
|
||||
(FalGptImage1Quality::Low, true) => 2,
|
||||
(FalGptImage1Quality::Low, false) => 2,
|
||||
(FalGptImage1Quality::Medium, true) => 5,
|
||||
(FalGptImage1Quality::Medium, false) => 7,
|
||||
(FalGptImage1Quality::High, true) => 17,
|
||||
(FalGptImage1Quality::High, false) => 25,
|
||||
};
|
||||
|
||||
let num_output_images = plan.num_images.as_u64();
|
||||
let output_cost = output_cost_per_image * num_output_images;
|
||||
|
||||
// Input image token cost: 2¢ per input image (high-fidelity estimate).
|
||||
let input_image_cost: u64 = if plan.image_urls.is_empty() {
|
||||
0
|
||||
} else {
|
||||
2 * plan.image_urls.len() as u64
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = output_cost + input_image_cost;
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::FalGptImage1NumImages;
|
||||
|
||||
fn text_plan(
|
||||
quality: FalGptImage1Quality,
|
||||
image_size: Option<FalGptImage1ImageSize>,
|
||||
num_images: FalGptImage1NumImages,
|
||||
) -> PlanFalGptImage1<'static> {
|
||||
PlanFalGptImage1 {
|
||||
prompt: Some("p"),
|
||||
image_urls: vec![],
|
||||
image_size,
|
||||
quality,
|
||||
num_images,
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_plan(
|
||||
quality: FalGptImage1Quality,
|
||||
image_size: Option<FalGptImage1ImageSize>,
|
||||
num_images: FalGptImage1NumImages,
|
||||
num_input_images: usize,
|
||||
) -> PlanFalGptImage1<'static> {
|
||||
let image_urls = (0..num_input_images)
|
||||
.map(|i| format!("https://fake.example.com/img{}.png", i))
|
||||
.collect();
|
||||
PlanFalGptImage1 {
|
||||
prompt: Some("p"),
|
||||
image_urls,
|
||||
image_size,
|
||||
quality,
|
||||
num_images,
|
||||
}
|
||||
}
|
||||
|
||||
fn cents_text(
|
||||
quality: FalGptImage1Quality,
|
||||
image_size: Option<FalGptImage1ImageSize>,
|
||||
num_images: FalGptImage1NumImages,
|
||||
) -> u64 {
|
||||
estimate_image_cost_fal_gpt_image_1(&text_plan(quality, image_size, num_images))
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn cents_edit(
|
||||
quality: FalGptImage1Quality,
|
||||
image_size: Option<FalGptImage1ImageSize>,
|
||||
num_images: FalGptImage1NumImages,
|
||||
num_input_images: usize,
|
||||
) -> u64 {
|
||||
estimate_image_cost_fal_gpt_image_1(&edit_plan(quality, image_size, num_images, num_input_images))
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// ── Low quality text-to-image (2¢/image, both sizes) ──────────────────
|
||||
|
||||
#[test]
|
||||
fn low_square_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_horizontal_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_vertical_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_unset_size_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Low, None, FalGptImage1NumImages::One), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_four_images() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Low, None, FalGptImage1NumImages::Four), 8);
|
||||
}
|
||||
|
||||
// ── Medium quality text-to-image (5¢ square, 7¢ wide/tall) ────────────
|
||||
|
||||
#[test]
|
||||
fn medium_square_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_horizontal_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_vertical_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_square_four_images() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::Four), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_horizontal_four_images() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Four), 28);
|
||||
}
|
||||
|
||||
// ── High quality text-to-image (17¢ square, 25¢ wide/tall) ────────────
|
||||
|
||||
#[test]
|
||||
fn high_square_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_horizontal_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_vertical_one_image() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_two_images() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::Two), 34);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_horizontal_four_images() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Four), 100);
|
||||
}
|
||||
|
||||
// ── Edit mode adds 2¢ per input image ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn edit_high_square_one_output_one_input() {
|
||||
// 17¢ output + 2¢ input = 19¢
|
||||
assert_eq!(cents_edit(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One, 1), 19);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_high_square_one_output_three_inputs() {
|
||||
// 17¢ output + 3×2¢ input = 23¢
|
||||
assert_eq!(cents_edit(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One, 3), 23);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_medium_wide_two_outputs_five_inputs() {
|
||||
// 2×7¢ output + 5×2¢ input = 24¢
|
||||
assert_eq!(cents_edit(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Two, 5), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_low_square_one_output_two_inputs() {
|
||||
// 2¢ output + 2×2¢ input = 6¢
|
||||
assert_eq!(cents_edit(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One, 2), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_high_vertical_four_outputs_one_input() {
|
||||
// 4×25¢ output + 1×2¢ input = 102¢
|
||||
assert_eq!(cents_edit(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::Four, 1), 102);
|
||||
}
|
||||
|
||||
// ── Text mode has no input image cost ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn text_mode_no_input_cost() {
|
||||
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 17);
|
||||
}
|
||||
|
||||
// ── Metadata flags ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn metadata_flags_are_default() {
|
||||
let estimate = estimate_image_cost_fal_gpt_image_1(&text_plan(
|
||||
FalGptImage1Quality::Medium,
|
||||
None,
|
||||
FalGptImage1NumImages::One,
|
||||
));
|
||||
assert!(!estimate.is_free);
|
||||
assert!(!estimate.is_unlimited);
|
||||
assert!(!estimate.is_rate_limited);
|
||||
assert!(!estimate.has_watermark);
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::{
|
||||
FalGptImage1p5ImageSize, FalGptImage1p5Quality, PlanFalGptImage1p5,
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_gpt_image_1p5(
|
||||
plan: &PlanFalGptImage1p5<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Per fal docs (fal-ai/gpt-image-1.5 and fal-ai/gpt-image-1.5/edit):
|
||||
//
|
||||
// Output image cost (per output image), rounded up to whole cents:
|
||||
// Low: $0.009 (1024×1024) / $0.013 (other) → 1¢ / 2¢
|
||||
// Medium: $0.034 (1024×1024) / $0.050 (1536×1024) / $0.051 (1024×1536) → 4¢ / 5¢ / 6¢
|
||||
// High: $0.133 (1024×1024) / $0.199 (1536×1024) / $0.200 (1024×1536) → 14¢ / 20¢ / 20¢
|
||||
//
|
||||
use FalGptImage1p5ImageSize as S;
|
||||
|
||||
let cost_per_image: u64 = match (plan.quality, plan.image_size) {
|
||||
// Low quality
|
||||
(FalGptImage1p5Quality::Low, None | Some(S::Square)) => 1,
|
||||
(FalGptImage1p5Quality::Low, Some(S::Wide) | Some(S::Tall)) => 2,
|
||||
|
||||
// Medium quality
|
||||
(FalGptImage1p5Quality::Medium, None | Some(S::Square)) => 4,
|
||||
(FalGptImage1p5Quality::Medium, Some(S::Wide)) => 5,
|
||||
(FalGptImage1p5Quality::Medium, Some(S::Tall)) => 6,
|
||||
|
||||
// High quality
|
||||
(FalGptImage1p5Quality::High, None | Some(S::Square)) => 14,
|
||||
(FalGptImage1p5Quality::High, Some(S::Wide) | Some(S::Tall)) => 20,
|
||||
};
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::FalGptImage1p5NumImages;
|
||||
|
||||
fn plan(
|
||||
quality: FalGptImage1p5Quality,
|
||||
image_size: Option<FalGptImage1p5ImageSize>,
|
||||
num_images: FalGptImage1p5NumImages,
|
||||
) -> PlanFalGptImage1p5<'static> {
|
||||
PlanFalGptImage1p5 {
|
||||
prompt: Some("p"),
|
||||
image_urls: vec![],
|
||||
image_size,
|
||||
quality,
|
||||
num_images,
|
||||
}
|
||||
}
|
||||
|
||||
fn cents(
|
||||
quality: FalGptImage1p5Quality,
|
||||
image_size: Option<FalGptImage1p5ImageSize>,
|
||||
num_images: FalGptImage1p5NumImages,
|
||||
) -> u64 {
|
||||
estimate_image_cost_fal_gpt_image_1p5(&plan(quality, image_size, num_images))
|
||||
.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// ── Low quality (1¢ square, 2¢ wide/tall) ─────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn low_square() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Low, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::One), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_unset() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Low, None, FalGptImage1p5NumImages::One), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_wide() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Low, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::One), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_tall() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Low, Some(FalGptImage1p5ImageSize::Tall), FalGptImage1p5NumImages::One), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_four() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Low, None, FalGptImage1p5NumImages::Four), 4);
|
||||
}
|
||||
|
||||
// ── Medium quality (4¢ square, 5¢ wide, 6¢ tall) ─────────────────────
|
||||
|
||||
#[test]
|
||||
fn medium_square() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::One), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_unset() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, None, FalGptImage1p5NumImages::One), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_wide() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::One), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_tall() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Tall), FalGptImage1p5NumImages::One), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_square_four() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::Four), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_wide_four() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::Four), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_tall_four() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Tall), FalGptImage1p5NumImages::Four), 24);
|
||||
}
|
||||
|
||||
// ── High quality (14¢ square, 20¢ wide/tall) ─────────────────────────
|
||||
|
||||
#[test]
|
||||
fn high_square() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::One), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_unset() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::High, None, FalGptImage1p5NumImages::One), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wide() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::One), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_tall() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Tall), FalGptImage1p5NumImages::One), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_four() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::Four), 56);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wide_four() {
|
||||
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::Four), 80);
|
||||
}
|
||||
|
||||
// ── Metadata flags ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn metadata_flags_are_default() {
|
||||
let estimate = estimate_image_cost_fal_gpt_image_1p5(&plan(
|
||||
FalGptImage1p5Quality::Medium, None, FalGptImage1p5NumImages::One,
|
||||
));
|
||||
assert!(!estimate.is_free);
|
||||
assert!(!estimate.is_unlimited);
|
||||
assert!(!estimate.is_rate_limited);
|
||||
assert!(!estimate.has_watermark);
|
||||
}
|
||||
}
|
||||
+1
@@ -48,6 +48,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::PlanFalSeedream4;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_seedream_4(
|
||||
plan: &PlanFalSeedream4<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.03 per image.
|
||||
let cost_per_image: u64 = 3;
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::PlanFalSeedream4p5;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_seedream_4p5(
|
||||
plan: &PlanFalSeedream4p5<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04 per image.
|
||||
let cost_per_image: u64 = 4;
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::PlanFalSeedream5Lite;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_seedream_5_lite(
|
||||
plan: &PlanFalSeedream5Lite<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.035 per image — round up to 4 cents.
|
||||
let cost_per_image: u64 = 4;
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
+7
@@ -1,5 +1,12 @@
|
||||
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_flux_pro_1p1;
|
||||
pub(crate) mod estimate_image_cost_fal_flux_pro_1p1_ultra;
|
||||
pub(crate) mod estimate_image_cost_fal_gpt_image_1;
|
||||
pub(crate) mod estimate_image_cost_fal_gpt_image_1p5;
|
||||
pub(crate) mod estimate_image_cost_fal_seedream_4;
|
||||
pub(crate) mod estimate_image_cost_fal_seedream_4p5;
|
||||
pub(crate) mod estimate_image_cost_fal_seedream_5_lite;
|
||||
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;
|
||||
|
||||
+3
@@ -47,6 +47,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_flux_1_dev_image_request()
|
||||
@@ -69,6 +70,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
..base_flux_1_dev_image_request()
|
||||
@@ -94,6 +96,7 @@ mod tests {
|
||||
println!("--- text-to-image aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a bear walking through an abandoned desert city at night"),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
|
||||
+3
-1
@@ -37,7 +37,6 @@ mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::{base_flux_1_schnell_image_request, get_artcraft_client};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
@@ -47,6 +46,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a man and woman walking through a cyberpunk city at night"),
|
||||
..base_flux_1_schnell_image_request()
|
||||
@@ -69,6 +69,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
..base_flux_1_schnell_image_request()
|
||||
@@ -94,6 +95,7 @@ mod tests {
|
||||
println!("--- text-to-image aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a man and woman walking through a futuristic city at evening"),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
|
||||
+1
@@ -51,6 +51,7 @@ mod tests {
|
||||
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::SquareHd),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
horizontal_angle: Some(30.0),
|
||||
|
||||
+3
@@ -47,6 +47,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_flux_pro_1p1_image_request()
|
||||
@@ -69,6 +70,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
..base_flux_pro_1p1_image_request()
|
||||
@@ -94,6 +96,7 @@ mod tests {
|
||||
println!("--- text-to-image aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a bear walking through an abandoned desert city at night"),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
|
||||
+3
@@ -47,6 +47,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_flux_pro_1p1_ultra_image_request()
|
||||
@@ -69,6 +70,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
..base_flux_pro_1p1_ultra_image_request()
|
||||
@@ -94,6 +96,7 @@ mod tests {
|
||||
println!("--- text-to-image aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a t-rex walking through an abandoned desert city at night"),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
ArtcraftImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1::PlanArtcraftGptImage1;
|
||||
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::GptImage1EditImageRequest;
|
||||
use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageRequest;
|
||||
use artcraft_client::endpoints::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
|
||||
use artcraft_client::endpoints::generate::image::text::generate_gpt_image_1_text_to_image::generate_gpt_image_1_text_to_image;
|
||||
|
||||
/// GPT Image 1 has two distinct legacy storyteller-web endpoints (one for
|
||||
/// text-to-image, one for image-edit), so we dispatch on whether image refs
|
||||
/// are present rather than calling a single multi-function endpoint.
|
||||
pub async fn execute_artcraft_gpt_image_1(
|
||||
plan: &PlanArtcraftGptImage1<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let inference_job_token = match plan.image_inputs {
|
||||
Some(image_inputs) => {
|
||||
let request = GptImage1EditImageRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
image_media_tokens: Some(image_inputs.to_owned()),
|
||||
image_size: plan.image_size.map(|s| s.to_edit()),
|
||||
num_images: Some(plan.num_images.to_edit()),
|
||||
image_quality: Some(plan.quality.to_edit()),
|
||||
};
|
||||
let response = gpt_image_1_edit_image(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
response.inference_job_token
|
||||
}
|
||||
None => {
|
||||
let request = GenerateGptImage1TextToImageRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
num_images: Some(plan.num_images.to_t2i()),
|
||||
image_quality: Some(plan.quality.to_t2i()),
|
||||
};
|
||||
let response = generate_gpt_image_1_text_to_image(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
response.inference_job_token
|
||||
}
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
|
||||
inference_job_token,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::{base_gpt_image_1_image_request, get_artcraft_client};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
// Build-only smoke test that exercises the full text-to-image plan path
|
||||
// (no network I/O — we never call generate_image).
|
||||
#[test]
|
||||
fn build_text_to_image_plan_smoke() {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_batch_count: Some(1),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
let plan = request.build().expect("plan should build");
|
||||
assert!(matches!(plan, ImageGenerationPlan::ArtcraftGptImage1(_)));
|
||||
}
|
||||
|
||||
// Build-only smoke test for the edit path. We can't actually fire the
|
||||
// request without an authenticated client, but we want to make sure the
|
||||
// dispatcher recognizes media-token inputs as edit-mode.
|
||||
#[test]
|
||||
fn build_edit_image_plan_smoke() {
|
||||
let tokens = vec![MediaFileToken::new_from_str("mf_test00000000000000000000000000")];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
let plan = request.build().expect("plan should build");
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = plan else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(plan.image_inputs.is_some(), "edit mode must carry image_inputs");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_image_gpt_image_1() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a horse walking through a cyberpunk city at night"),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
|
||||
println!("Job token: {:?}", payload.inference_job_token);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_edit_image_gpt_image_1() {
|
||||
let client = get_artcraft_client();
|
||||
|
||||
let image_tokens = vec![
|
||||
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
|
||||
];
|
||||
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Change the background to a desert with cacti"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_batch_count: Some(1),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
|
||||
println!("Job token: {:?}", payload.inference_job_token);
|
||||
}
|
||||
}
|
||||
+4
@@ -50,6 +50,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_nano_banana_image_request()
|
||||
@@ -79,6 +80,7 @@ mod tests {
|
||||
prompt: Some("Change the background to a sunny beach"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
..base_nano_banana_image_request()
|
||||
};
|
||||
@@ -103,6 +105,7 @@ mod tests {
|
||||
println!("--- text-to-image aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a shiba walking through a cyberpunk city at night"),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
@@ -138,6 +141,7 @@ mod tests {
|
||||
prompt: Some("Change the background to a sunny beach"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_nano_banana_image_request()
|
||||
|
||||
+1
@@ -49,6 +49,7 @@ mod tests {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution: Some(CommonResolution::OneK),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_nano_banana_2_image_request()
|
||||
|
||||
+3
@@ -51,6 +51,7 @@ mod tests {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution: Some(CommonResolution::OneK),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_image_request()
|
||||
@@ -74,6 +75,7 @@ mod tests {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
resolution: Some(CommonResolution::OneK),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
..base_image_request()
|
||||
@@ -106,6 +108,7 @@ mod tests {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions
|
||||
resolution: Some(CommonResolution::OneK),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
..base_image_request()
|
||||
};
|
||||
|
||||
+1
@@ -52,6 +52,7 @@ mod tests {
|
||||
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::SquareHd),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
|
||||
+4
@@ -51,6 +51,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_seedream_4_image_request()
|
||||
@@ -80,6 +81,7 @@ mod tests {
|
||||
prompt: Some("Change the background to a sunny beach"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
..base_seedream_4_image_request()
|
||||
};
|
||||
@@ -104,6 +106,7 @@ mod tests {
|
||||
println!("--- text-to-image aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a shiba walking through a cyberpunk city at night"),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
@@ -139,6 +142,7 @@ mod tests {
|
||||
prompt: Some("Change the background to a sunny beach"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_seedream_4_image_request()
|
||||
|
||||
+4
@@ -51,6 +51,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a corgi walking through a cyberpunk city at night"),
|
||||
..base_seedream_4p5_image_request()
|
||||
@@ -80,6 +81,7 @@ mod tests {
|
||||
prompt: Some("Change the background to a spooky haunted house"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto2k),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
..base_seedream_4p5_image_request()
|
||||
};
|
||||
@@ -104,6 +106,7 @@ mod tests {
|
||||
println!("--- text-to-image aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a corgi walking through a cyberpunk city at night"),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
@@ -139,6 +142,7 @@ mod tests {
|
||||
prompt: Some("Change the background to a spooky haunted house"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_seedream_4p5_image_request()
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ mod tests {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a corgi walking through a cyberpunk city at night"),
|
||||
..base_seedream_5_lite_image_request()
|
||||
|
||||
+1
@@ -2,6 +2,7 @@ pub mod generate_image_artcraft_flux_1_dev;
|
||||
pub mod generate_image_artcraft_flux_1_schnell;
|
||||
pub mod generate_image_artcraft_flux_pro_1p1;
|
||||
pub mod generate_image_artcraft_flux_pro_1p1_ultra;
|
||||
pub mod generate_image_artcraft_gpt_image_1;
|
||||
pub mod generate_image_artcraft_gpt_image_1p5;
|
||||
pub mod generate_image_artcraft_nano_banana;
|
||||
pub mod generate_image_artcraft_nano_banana_2;
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
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_pro_1p1::PlanFalFluxPro11;
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
|
||||
enqueue_flux_pro_11_text_to_image_webhook, FluxPro11Args,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_flux_pro_1p1(
|
||||
plan: &PlanFalFluxPro11<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let args = FluxPro11Args {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
num_images: plan.num_images.to_fal(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
|
||||
let webhook_response = enqueue_flux_pro_11_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,
|
||||
}))
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
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_pro_1p1_ultra::PlanFalFluxPro11Ultra;
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
|
||||
enqueue_flux_pro_11_ultra_text_to_image_webhook, FluxPro11UltraArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_flux_pro_1p1_ultra(
|
||||
plan: &PlanFalFluxPro11Ultra<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let args = FluxPro11UltraArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
num_images: plan.num_images.to_fal(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
|
||||
let webhook_response = enqueue_flux_pro_11_ultra_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,
|
||||
}))
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
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_gpt_image_1::PlanFalGptImage1;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1_edit_image_webhook::{
|
||||
enqueue_gpt_image_1_edit_image_webhook, EnqueueGptImage1EditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1_text_to_image_webhook::{
|
||||
enqueue_gpt_image_1_text_to_image_webhook, EnqueueGptImage1TextToImageArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_gpt_image_1(
|
||||
plan: &PlanFalGptImage1<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueGptImage1TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
num_images: plan.num_images.to_t2i(),
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
quality: Some(plan.quality.to_t2i()),
|
||||
background: None,
|
||||
output_format: None,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_gpt_image_1_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueGptImage1EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: plan.num_images.to_edit(),
|
||||
mask_image_url: None,
|
||||
image_size: plan.image_size.map(|s| s.to_edit()),
|
||||
quality: Some(plan.quality.to_edit()),
|
||||
input_fidelity: None,
|
||||
background: None,
|
||||
output_format: None,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_gpt_image_1_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,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::get_fal_client;
|
||||
|
||||
fn base_fal_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::GptImage1,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space"),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
// Build-only smoke test (no network I/O).
|
||||
#[test]
|
||||
fn build_text_to_image_plan_smoke() {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_batch_count: Some(1),
|
||||
..base_fal_request()
|
||||
};
|
||||
let plan = request.build().expect("plan should build");
|
||||
let ImageGenerationPlan::FalGptImage1(plan) = plan else {
|
||||
panic!("expected FalGptImage1")
|
||||
};
|
||||
assert!(plan.image_urls.is_empty(), "text mode must have no image_urls");
|
||||
}
|
||||
|
||||
// Build-only smoke test for edit mode.
|
||||
#[test]
|
||||
fn build_edit_image_plan_smoke() {
|
||||
let urls = vec!["https://example.com/img.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
..base_fal_request()
|
||||
};
|
||||
let plan = request.build().expect("plan should build");
|
||||
let ImageGenerationPlan::FalGptImage1(plan) = plan else {
|
||||
panic!("expected FalGptImage1")
|
||||
};
|
||||
assert_eq!(plan.image_urls, urls);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real fal API request and incurs cost
|
||||
async fn test_text_to_image_gpt_image_1_fal() {
|
||||
let client = get_fal_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a horse walking through a cyberpunk city at night"),
|
||||
..base_fal_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
println!("Request id: {:?}", payload.request_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real fal API request and incurs cost
|
||||
async fn test_edit_image_gpt_image_1_fal() {
|
||||
let client = get_fal_client();
|
||||
let urls = vec![
|
||||
TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("change the background to a desert"),
|
||||
..base_fal_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
println!("Request id: {:?}", payload.request_id);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
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_gpt_image_1p5::PlanFalGptImage1p5;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1p5_edit_image_webhook::{
|
||||
enqueue_gpt_image_1p5_image_edit_webhook, EnqueueGptImage1p5EditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1p5_text_to_image_webhook::{
|
||||
enqueue_gpt_image_1p5_text_to_image_webhook, EnqueueGptImage1p5TextToImageArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_gpt_image_1p5(
|
||||
plan: &PlanFalGptImage1p5<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueGptImage1p5TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
num_images: plan.num_images.to_t2i(),
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
background: None,
|
||||
quality: Some(plan.quality.to_t2i()),
|
||||
output_format: None,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_gpt_image_1p5_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueGptImage1p5EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: plan.num_images.to_edit(),
|
||||
mask_image_url: None,
|
||||
image_size: plan.image_size.map(|s| s.to_edit()),
|
||||
background: None,
|
||||
quality: Some(plan.quality.to_edit()),
|
||||
input_fidelity: None,
|
||||
output_format: None,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_gpt_image_1p5_image_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,
|
||||
}))
|
||||
}
|
||||
+3
@@ -105,6 +105,7 @@ mod tests {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution: Some(CommonResolution::TwoK),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cyberpunk city skyline at dusk, neon lights reflecting on rain-soaked streets"),
|
||||
..base_fal_image_request()
|
||||
@@ -129,6 +130,7 @@ mod tests {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
resolution: Some(CommonResolution::OneK),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a golden retriever surfing a wave, cinematic, 4K"),
|
||||
..base_fal_image_request()
|
||||
@@ -161,6 +163,7 @@ mod tests {
|
||||
image_inputs: Some(ImageListRef::Urls(&image_urls)),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions
|
||||
resolution: Some(CommonResolution::TwoK),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
..base_fal_image_request()
|
||||
};
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
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_seedream_4::PlanFalSeedream4;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::{
|
||||
enqueue_bytedance_seedream_v4_edit_image_webhook, EnqueueBytedanceSeedreamV4EditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{
|
||||
enqueue_bytedance_seedream_v4_text_to_image_webhook, EnqueueBytedanceSeedreamV4TextToImageArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_seedream_4(
|
||||
plan: &PlanFalSeedream4<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueBytedanceSeedreamV4TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
num_images: Some(plan.num_images.to_t2i()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_bytedance_seedream_v4_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueBytedanceSeedreamV4EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: Some(plan.num_images.to_edit()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_edit()),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_bytedance_seedream_v4_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,
|
||||
}))
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
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_seedream_4p5::PlanFalSeedream4p5;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{
|
||||
enqueue_bytedance_seedream_v4p5_edit_image_webhook, EnqueueBytedanceSeedreamV4p5EditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{
|
||||
enqueue_bytedance_seedream_v4p5_text_to_image_webhook, EnqueueBytedanceSeedreamV4p5TextToImageArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_seedream_4p5(
|
||||
plan: &PlanFalSeedream4p5<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueBytedanceSeedreamV4p5TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
num_images: Some(plan.num_images.to_t2i()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_bytedance_seedream_v4p5_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueBytedanceSeedreamV4p5EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: Some(plan.num_images.to_edit()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_edit()),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_bytedance_seedream_v4p5_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,
|
||||
}))
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
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_seedream_5_lite::PlanFalSeedream5Lite;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v5_lite_edit_image_webhook::{
|
||||
enqueue_bytedance_seedream_v5_lite_edit_image_webhook, EnqueueBytedanceSeedreamV5LiteEditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::{
|
||||
enqueue_bytedance_seedream_v5_lite_text_to_image_webhook, EnqueueBytedanceSeedreamV5LiteTextToImageArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_seedream_5_lite(
|
||||
plan: &PlanFalSeedream5Lite<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueBytedanceSeedreamV5LiteTextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
num_images: Some(plan.num_images.to_t2i()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_bytedance_seedream_v5_lite_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueBytedanceSeedreamV5LiteEditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: Some(plan.num_images.to_edit()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_edit()),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_bytedance_seedream_v5_lite_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,
|
||||
}))
|
||||
}
|
||||
+7
@@ -1,5 +1,12 @@
|
||||
pub mod generate_image_fal_flux_1_dev;
|
||||
pub mod generate_image_fal_flux_1_schnell;
|
||||
pub mod generate_image_fal_flux_pro_1p1;
|
||||
pub mod generate_image_fal_flux_pro_1p1_ultra;
|
||||
pub mod generate_image_fal_gpt_image_1;
|
||||
pub mod generate_image_fal_gpt_image_1p5;
|
||||
pub mod generate_image_fal_seedream_4;
|
||||
pub mod generate_image_fal_seedream_4p5;
|
||||
pub mod generate_image_fal_seedream_5_lite;
|
||||
pub mod generate_image_fal_nano_banana;
|
||||
pub mod generate_image_fal_nano_banana_2;
|
||||
pub mod generate_image_fal_nano_banana_pro;
|
||||
|
||||
+21
-1
@@ -1,5 +1,6 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::provider::Provider;
|
||||
@@ -13,6 +14,7 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
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_1::plan_generate_image_artcraft_gpt_image_1;
|
||||
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;
|
||||
@@ -23,11 +25,18 @@ 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::plan_generate_image_artcraft_seedream_5_lite;
|
||||
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_flux_pro_1p1::plan_generate_image_fal_flux_pro_1p1;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::plan_generate_image_fal_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::plan_generate_image_fal_gpt_image_1;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::plan_generate_image_fal_gpt_image_1p5;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::plan_generate_image_fal_seedream_4;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::plan_generate_image_fal_seedream_4p5;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::plan_generate_image_fal_seedream_5_lite;
|
||||
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)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateImageRequest<'a> {
|
||||
/// Which model to use.
|
||||
pub model: CommonImageModel,
|
||||
@@ -49,6 +58,9 @@ pub struct GenerateImageRequest<'a> {
|
||||
/// The aspect ratio to use.
|
||||
pub aspect_ratio: Option<CommonAspectRatio>,
|
||||
|
||||
/// The quality level for generation. Not all models use this.
|
||||
pub quality: Option<CommonQuality>,
|
||||
|
||||
/// How many images to generate.
|
||||
pub image_batch_count: Option<u16>,
|
||||
|
||||
@@ -90,6 +102,7 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
CommonImageModel::Flux1Schnell => plan_generate_image_artcraft_flux_1_schnell(self),
|
||||
CommonImageModel::FluxPro11 => plan_generate_image_artcraft_flux_pro_1p1(self),
|
||||
CommonImageModel::FluxPro11Ultra => plan_generate_image_artcraft_flux_pro_1p1_ultra(self),
|
||||
CommonImageModel::GptImage1 => plan_generate_image_artcraft_gpt_image_1(self),
|
||||
CommonImageModel::GptImage1p5 => plan_generate_image_artcraft_gpt_image_1p5(self),
|
||||
CommonImageModel::NanoBanana => plan_generate_image_artcraft_nano_banana(self),
|
||||
CommonImageModel::NanoBanana2 => plan_generate_image_artcraft_nano_banana_2(self),
|
||||
@@ -106,6 +119,13 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
match self.model {
|
||||
CommonImageModel::Flux1Dev => plan_generate_image_fal_flux_1_dev(self),
|
||||
CommonImageModel::Flux1Schnell => plan_generate_image_fal_flux_1_schnell(self),
|
||||
CommonImageModel::FluxPro11 => plan_generate_image_fal_flux_pro_1p1(self),
|
||||
CommonImageModel::FluxPro11Ultra => plan_generate_image_fal_flux_pro_1p1_ultra(self),
|
||||
CommonImageModel::GptImage1 => plan_generate_image_fal_gpt_image_1(self),
|
||||
CommonImageModel::GptImage1p5 => plan_generate_image_fal_gpt_image_1p5(self),
|
||||
CommonImageModel::Seedream4 => plan_generate_image_fal_seedream_4(self),
|
||||
CommonImageModel::Seedream4p5 => plan_generate_image_fal_seedream_4p5(self),
|
||||
CommonImageModel::Seedream5Lite => plan_generate_image_fal_seedream_5_lite(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),
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
/// The result of estimating the cost of an image generation plan.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ImageGenerationCostEstimate {
|
||||
pub cost_in_credits: Option<u64>,
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
|
||||
+88
@@ -4,6 +4,7 @@ use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraf
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_1_schnell::estimate_image_cost_artcraft_flux_1_schnell;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_pro_1p1::estimate_image_cost_artcraft_flux_pro_1p1;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_pro_1p1_ultra::estimate_image_cost_artcraft_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_gpt_image_1::estimate_image_cost_artcraft_gpt_image_1;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_gpt_image_1p5::estimate_image_cost_artcraft_gpt_image_1p5;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_nano_banana::estimate_image_cost_artcraft_nano_banana;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_nano_banana_2::estimate_image_cost_artcraft_nano_banana_2;
|
||||
@@ -15,6 +16,13 @@ use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraf
|
||||
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_flux_pro_1p1::estimate_image_cost_fal_flux_pro_1p1;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_pro_1p1_ultra::estimate_image_cost_fal_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_gpt_image_1::estimate_image_cost_fal_gpt_image_1;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_gpt_image_1p5::estimate_image_cost_fal_gpt_image_1p5;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_seedream_4::estimate_image_cost_fal_seedream_4;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_seedream_4p5::estimate_image_cost_fal_seedream_4p5;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_seedream_5_lite::estimate_image_cost_fal_seedream_5_lite;
|
||||
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;
|
||||
@@ -22,6 +30,7 @@ use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_schnell::execute_artcraft_flux_1_schnell;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_pro_1p1::execute_artcraft_flux_pro_1p1;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_pro_1p1_ultra::execute_artcraft_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_gpt_image_1::execute_artcraft_gpt_image_1;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_gpt_image_1p5::execute_artcraft_gpt_image_1p5;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana::execute_artcraft_nano_banana;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana_2::execute_artcraft_nano_banana_2;
|
||||
@@ -33,6 +42,13 @@ use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_
|
||||
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_flux_pro_1p1::execute_fal_flux_pro_1p1;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_pro_1p1_ultra::execute_fal_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_gpt_image_1::execute_fal_gpt_image_1;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_gpt_image_1p5::execute_fal_gpt_image_1p5;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_seedream_4::execute_fal_seedream_4;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_seedream_4p5::execute_fal_seedream_4p5;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_seedream_5_lite::execute_fal_seedream_5_lite;
|
||||
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;
|
||||
@@ -42,6 +58,7 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_schnell::PlanArtcraftFlux1Schnell;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1::PlanArtcraftFluxPro11;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::PlanArtcraftFluxPro11Ultra;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1::PlanArtcraftGptImage1;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::PlanArtcraftGptImage1p5;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::PlanArtcraftNanaBanana;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::PlanArtcraftNanaBanana2;
|
||||
@@ -53,6 +70,13 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
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_flux_pro_1p1::PlanFalFluxPro11;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::PlanFalFluxPro11Ultra;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::PlanFalGptImage1;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::PlanFalGptImage1p5;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::PlanFalSeedream4;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::PlanFalSeedream4p5;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::PlanFalSeedream5Lite;
|
||||
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;
|
||||
@@ -63,6 +87,7 @@ pub enum ImageGenerationPlan<'a> {
|
||||
ArtcraftFlux1Schnell(PlanArtcraftFlux1Schnell<'a>),
|
||||
ArtcraftFluxPro11(PlanArtcraftFluxPro11<'a>),
|
||||
ArtcraftFluxPro11Ultra(PlanArtcraftFluxPro11Ultra<'a>),
|
||||
ArtcraftGptImage1(PlanArtcraftGptImage1<'a>),
|
||||
ArtcraftGptImage1p5(PlanArtcraftGptImage1p5<'a>),
|
||||
ArtcraftNanaBanana(PlanArtcraftNanaBanana<'a>),
|
||||
ArtcraftNanaBanana2(PlanArtcraftNanaBanana2<'a>),
|
||||
@@ -74,6 +99,13 @@ pub enum ImageGenerationPlan<'a> {
|
||||
ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles<'a>),
|
||||
FalFlux1Dev(PlanFalFlux1Dev<'a>),
|
||||
FalFlux1Schnell(PlanFalFlux1Schnell<'a>),
|
||||
FalFluxPro11(PlanFalFluxPro11<'a>),
|
||||
FalFluxPro11Ultra(PlanFalFluxPro11Ultra<'a>),
|
||||
FalGptImage1(PlanFalGptImage1<'a>),
|
||||
FalGptImage1p5(PlanFalGptImage1p5<'a>),
|
||||
FalSeedream4(PlanFalSeedream4<'a>),
|
||||
FalSeedream4p5(PlanFalSeedream4p5<'a>),
|
||||
FalSeedream5Lite(PlanFalSeedream5Lite<'a>),
|
||||
FalNanoBanana(PlanFalNanoBanana<'a>),
|
||||
FalNanaBanana2(PlanFalNanaBanana2<'a>),
|
||||
FalNanaBananaPro(PlanFalNanaBananaPro<'a>),
|
||||
@@ -101,6 +133,10 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_flux_pro_1p1_ultra(plan, artcraft_client).await
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftGptImage1(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_gpt_image_1(plan, artcraft_client).await
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftGptImage1p5(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_gpt_image_1p5(plan, artcraft_client).await
|
||||
@@ -145,6 +181,34 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_flux_1_schnell(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalFluxPro11(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_flux_pro_1p1(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalFluxPro11Ultra(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_flux_pro_1p1_ultra(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalGptImage1(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_gpt_image_1(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalGptImage1p5(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_gpt_image_1p5(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream4(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_seedream_4(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream4p5(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_seedream_4p5(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream5Lite(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_seedream_5_lite(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalNanoBanana(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_nano_banana(plan, fal_client).await
|
||||
@@ -174,6 +238,9 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) => {
|
||||
estimate_image_cost_artcraft_flux_pro_1p1_ultra(plan)
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftGptImage1(plan) => {
|
||||
estimate_image_cost_artcraft_gpt_image_1(plan)
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftGptImage1p5(plan) => {
|
||||
estimate_image_cost_artcraft_gpt_image_1p5(plan)
|
||||
}
|
||||
@@ -207,6 +274,27 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
ImageGenerationPlan::FalFlux1Schnell(plan) => {
|
||||
estimate_image_cost_fal_flux_1_schnell(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalFluxPro11(plan) => {
|
||||
estimate_image_cost_fal_flux_pro_1p1(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalFluxPro11Ultra(plan) => {
|
||||
estimate_image_cost_fal_flux_pro_1p1_ultra(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalGptImage1(plan) => {
|
||||
estimate_image_cost_fal_gpt_image_1(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalGptImage1p5(plan) => {
|
||||
estimate_image_cost_fal_gpt_image_1p5(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream4(plan) => {
|
||||
estimate_image_cost_fal_seedream_4(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream4p5(plan) => {
|
||||
estimate_image_cost_fal_seedream_4p5(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream5Lite(plan) => {
|
||||
estimate_image_cost_fal_seedream_5_lite(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalNanoBanana(plan) => {
|
||||
estimate_image_cost_fal_nano_banana(plan)
|
||||
}
|
||||
|
||||
+1
@@ -2,6 +2,7 @@ pub mod plan_generate_image_artcraft_flux_1_dev;
|
||||
pub mod plan_generate_image_artcraft_flux_1_schnell;
|
||||
pub mod plan_generate_image_artcraft_flux_pro_1p1;
|
||||
pub mod plan_generate_image_artcraft_flux_pro_1p1_ultra;
|
||||
pub mod plan_generate_image_artcraft_gpt_image_1;
|
||||
pub mod plan_generate_image_artcraft_gpt_image_1p5;
|
||||
pub mod plan_generate_image_artcraft_nano_banana;
|
||||
pub mod plan_generate_image_artcraft_nano_banana_2;
|
||||
|
||||
+3
@@ -268,6 +268,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_flux_1_dev_image_request()
|
||||
@@ -295,6 +296,7 @@ mod tests {
|
||||
#[test]
|
||||
fn num_images_out_of_range_error_out() {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_flux_1_dev_image_request()
|
||||
@@ -312,6 +314,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_flux_1_dev_image_request()
|
||||
|
||||
+3
@@ -264,6 +264,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_flux_1_schnell_image_request()
|
||||
@@ -291,6 +292,7 @@ mod tests {
|
||||
#[test]
|
||||
fn num_images_out_of_range_error_out() {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_flux_1_schnell_image_request()
|
||||
@@ -308,6 +310,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_flux_1_schnell_image_request()
|
||||
|
||||
+2
-1
@@ -80,7 +80,7 @@ fn plan_image_size(
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto4k) => {
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) | Some(CommonAspectRatio::Auto4k) => {
|
||||
Ok(Some(S::SquareHd))
|
||||
}
|
||||
|
||||
@@ -224,6 +224,7 @@ mod tests {
|
||||
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
|
||||
for (count, expected) in cases {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(count),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
|
||||
+3
@@ -268,6 +268,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_flux_pro_1p1_image_request()
|
||||
@@ -295,6 +296,7 @@ mod tests {
|
||||
#[test]
|
||||
fn num_images_out_of_range_error_out() {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_flux_pro_1p1_image_request()
|
||||
@@ -312,6 +314,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_flux_pro_1p1_image_request()
|
||||
|
||||
+3
@@ -271,6 +271,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_flux_pro_1p1_ultra_image_request()
|
||||
@@ -298,6 +299,7 @@ mod tests {
|
||||
#[test]
|
||||
fn num_images_out_of_range_error_out() {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_flux_pro_1p1_ultra_image_request()
|
||||
@@ -315,6 +317,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_flux_pro_1p1_ultra_image_request()
|
||||
|
||||
+537
@@ -0,0 +1,537 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
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 artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{
|
||||
GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages,
|
||||
};
|
||||
use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::{
|
||||
GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize,
|
||||
GenerateGptImage1TextToImageNumImages,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Quality is shared between t2i and edit (Auto / Low / Medium / High).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ArtcraftGptImage1Quality {
|
||||
Auto,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
/// Image size is shared between t2i and edit (Square / Horizontal / Vertical).
|
||||
/// GPT Image 1 has no native auto size — None means "let the legacy handler
|
||||
/// pick its default" (which is Square).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ArtcraftGptImage1Size {
|
||||
Square,
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ArtcraftGptImage1NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftGptImage1<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Input images for image editing. None means text-to-image mode.
|
||||
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
|
||||
/// Number of input images (0 for text-to-image). This is counted from either
|
||||
/// media tokens or hydrated URLs, so the cost estimator can use it without
|
||||
/// needing to inspect image_inputs (which is None for URL-form inputs).
|
||||
pub num_input_images: u64,
|
||||
pub image_size: Option<ArtcraftGptImage1Size>,
|
||||
/// Quality defaults to High when the request leaves it unspecified, matching
|
||||
/// the legacy storyteller-web handler defaults.
|
||||
pub quality: ArtcraftGptImage1Quality,
|
||||
pub num_images: ArtcraftGptImage1NumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_gpt_image_1<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
|
||||
let num_input_images = count_input_images(request.image_inputs);
|
||||
let image_size = plan_image_size(request.aspect_ratio);
|
||||
let quality = plan_quality(request.quality);
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftGptImage1(PlanArtcraftGptImage1 {
|
||||
prompt: request.prompt,
|
||||
image_inputs,
|
||||
num_input_images,
|
||||
image_size,
|
||||
quality,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
// Omni-gen distillation hydrates media tokens to URLs before running the
|
||||
// Artcraft cost path. Cost only depends on quality + size + num_images +
|
||||
// is_edit_mode (derived before this resolver runs), so URL-form inputs are
|
||||
// accepted and dropped.
|
||||
Some(ImageListRef::Urls(_)) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn count_input_images(image_inputs: Option<ImageListRef<'_>>) -> u64 {
|
||||
match image_inputs {
|
||||
None => 0,
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => tokens.len() as u64,
|
||||
Some(ImageListRef::Urls(urls)) => urls.len() as u64,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_quality(quality: Option<CommonQuality>) -> ArtcraftGptImage1Quality {
|
||||
match quality {
|
||||
Some(CommonQuality::Low) => ArtcraftGptImage1Quality::Low,
|
||||
Some(CommonQuality::Medium) => ArtcraftGptImage1Quality::Medium,
|
||||
Some(CommonQuality::High) => ArtcraftGptImage1Quality::High,
|
||||
None => ArtcraftGptImage1Quality::High,
|
||||
}
|
||||
}
|
||||
|
||||
// GPT Image 1 image sizes: Square (1024x1024), Horizontal (1536x1024),
|
||||
// Vertical (1024x1536). All 17 CommonAspectRatio variants map to one of these
|
||||
// or None (let the legacy handler default to Square).
|
||||
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<ArtcraftGptImage1Size> {
|
||||
use ArtcraftGptImage1Size as S;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => None,
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Some(S::Square),
|
||||
|
||||
Some(CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(CommonAspectRatio::WideFourByThree)
|
||||
| Some(CommonAspectRatio::WideFiveByFour)
|
||||
| Some(CommonAspectRatio::WideSixteenByNine)
|
||||
| Some(CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(CommonAspectRatio::Wide) => Some(S::Horizontal),
|
||||
|
||||
Some(CommonAspectRatio::TallTwoByThree)
|
||||
| Some(CommonAspectRatio::TallThreeByFour)
|
||||
| Some(CommonAspectRatio::TallFourByFive)
|
||||
| Some(CommonAspectRatio::TallNineBySixteen)
|
||||
| Some(CommonAspectRatio::TallNineByTwentyOne)
|
||||
| Some(CommonAspectRatio::Tall) => Some(S::Vertical),
|
||||
}
|
||||
}
|
||||
|
||||
// GPT Image 1 supports 1, 2, 3, and 4 images.
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<ArtcraftGptImage1NumImages, ArtcraftRouterError> {
|
||||
use ArtcraftGptImage1NumImages as N;
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(N::One),
|
||||
2 => Ok(N::Two),
|
||||
3 => Ok(N::Three),
|
||||
4 => Ok(N::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(N::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── Conversions to legacy storyteller-web request enums ────────────────────
|
||||
|
||||
impl ArtcraftGptImage1NumImages {
|
||||
pub fn to_t2i(self) -> GenerateGptImage1TextToImageNumImages {
|
||||
use GenerateGptImage1TextToImageNumImages as T;
|
||||
match self {
|
||||
Self::One => T::One,
|
||||
Self::Two => T::Two,
|
||||
Self::Three => T::Three,
|
||||
Self::Four => T::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> GptImage1EditImageNumImages {
|
||||
use GptImage1EditImageNumImages as E;
|
||||
match self {
|
||||
Self::One => E::One,
|
||||
Self::Two => E::Two,
|
||||
Self::Three => E::Three,
|
||||
Self::Four => E::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ArtcraftGptImage1Quality {
|
||||
pub fn to_t2i(self) -> GenerateGptImage1TextToImageImageQuality {
|
||||
use GenerateGptImage1TextToImageImageQuality as T;
|
||||
match self {
|
||||
Self::Auto => T::Auto,
|
||||
Self::Low => T::Low,
|
||||
Self::Medium => T::Medium,
|
||||
Self::High => T::High,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> GptImage1EditImageImageQuality {
|
||||
use GptImage1EditImageImageQuality as E;
|
||||
match self {
|
||||
Self::Auto => E::Auto,
|
||||
Self::Low => E::Low,
|
||||
Self::Medium => E::Medium,
|
||||
Self::High => E::High,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ArtcraftGptImage1Size {
|
||||
pub fn to_t2i(self) -> GenerateGptImage1TextToImageImageSize {
|
||||
use GenerateGptImage1TextToImageImageSize as T;
|
||||
match self {
|
||||
Self::Square => T::Square,
|
||||
Self::Horizontal => T::Horizontal,
|
||||
Self::Vertical => T::Vertical,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> GptImage1EditImageImageSize {
|
||||
use GptImage1EditImageImageSize as E;
|
||||
match self {
|
||||
Self::Square => E::Square,
|
||||
Self::Horizontal => E::Horizontal,
|
||||
Self::Vertical => E::Vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::test_helpers::base_gpt_image_1_image_request;
|
||||
|
||||
// ── Image size (aspect ratio mapping) ────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn image_size_none_is_none() {
|
||||
let request = GenerateImageRequest { aspect_ratio: None, ..base_gpt_image_1_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(plan.image_size.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_auto_variants_yield_none() {
|
||||
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
|
||||
let request = GenerateImageRequest { aspect_ratio: Some(auto_ar), ..base_gpt_image_1_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(plan.image_size.is_none(), "expected None for {:?}", auto_ar);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_square_variants() {
|
||||
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
|
||||
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(matches!(plan.image_size, Some(ArtcraftGptImage1Size::Square)), "expected Square for {:?}", ar);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_wide_variants_yield_horizontal() {
|
||||
let wide_ars = [
|
||||
CommonAspectRatio::WideThreeByTwo,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::WideFiveByFour,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::Wide,
|
||||
];
|
||||
for ar in wide_ars {
|
||||
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(matches!(plan.image_size, Some(ArtcraftGptImage1Size::Horizontal)), "expected Horizontal for {:?}", ar);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_tall_variants_yield_vertical() {
|
||||
let tall_ars = [
|
||||
CommonAspectRatio::TallTwoByThree,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallFourByFive,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
CommonAspectRatio::TallNineByTwentyOne,
|
||||
CommonAspectRatio::Tall,
|
||||
];
|
||||
for ar in tall_ars {
|
||||
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(matches!(plan.image_size, Some(ArtcraftGptImage1Size::Vertical)), "expected Vertical for {:?}", ar);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Quality default ──────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn quality_defaults_to_high() {
|
||||
let request = base_gpt_image_1_image_request();
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(matches!(plan.quality, ArtcraftGptImage1Quality::High));
|
||||
}
|
||||
|
||||
// ── Num images ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn num_images_zero_is_always_error() {
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::ErrorOut,
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(
|
||||
matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
|
||||
"expected UserRequestedZeroGenerations with {:?}", strategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_default_is_one() {
|
||||
let request = GenerateImageRequest { image_batch_count: None, ..base_gpt_image_1_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(matches!(plan.num_images, ArtcraftGptImage1NumImages::One));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_direct_mapping() {
|
||||
let cases = [
|
||||
(1, ArtcraftGptImage1NumImages::One),
|
||||
(2, ArtcraftGptImage1NumImages::Two),
|
||||
(3, ArtcraftGptImage1NumImages::Three),
|
||||
(4, ArtcraftGptImage1NumImages::Four),
|
||||
];
|
||||
for (count, expected) in cases {
|
||||
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_gpt_image_1_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(
|
||||
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
|
||||
"expected {:?} for count {}", expected, count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_out_of_range_error_out() {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_out_of_range_clamps_to_four() {
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(9),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(
|
||||
matches!(plan.num_images, ArtcraftGptImage1NumImages::Four),
|
||||
"expected Four for count 9 with {:?}", strategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Image inputs (mode detection) ────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn no_image_inputs_is_text_to_image_mode() {
|
||||
let request = GenerateImageRequest { image_inputs: None, ..base_gpt_image_1_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(plan.image_inputs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_token_image_inputs_is_edit_mode() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(plan.image_inputs.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_image_inputs_are_accepted_for_cost_path() {
|
||||
// The omni-gen distillation hydrates media tokens to URLs before running
|
||||
// cost estimation against Artcraft. URLs are accepted (and dropped) since
|
||||
// cost only depends on quality + size + num_images.
|
||||
let urls = vec!["https://example.com/image.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(plan.image_inputs.is_none());
|
||||
}
|
||||
|
||||
// ── Idempotency token ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn idempotency_token_is_generated_when_unset() {
|
||||
let request = base_gpt_image_1_image_request();
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert!(!plan.idempotency_token.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idempotency_token_passthrough() {
|
||||
let token = "11111111-1111-1111-1111-111111111111";
|
||||
let request = GenerateImageRequest { idempotency_token: Some(token), ..base_gpt_image_1_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
assert_eq!(plan.idempotency_token, token);
|
||||
}
|
||||
|
||||
// ── Conversions to legacy storyteller-web request enums ─────────────────
|
||||
|
||||
#[test]
|
||||
fn num_images_to_t2i_round_trip() {
|
||||
use GenerateGptImage1TextToImageNumImages as T;
|
||||
assert!(matches!(ArtcraftGptImage1NumImages::One.to_t2i(), T::One));
|
||||
assert!(matches!(ArtcraftGptImage1NumImages::Two.to_t2i(), T::Two));
|
||||
assert!(matches!(ArtcraftGptImage1NumImages::Three.to_t2i(), T::Three));
|
||||
assert!(matches!(ArtcraftGptImage1NumImages::Four.to_t2i(), T::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_to_edit_round_trip() {
|
||||
use GptImage1EditImageNumImages as E;
|
||||
assert!(matches!(ArtcraftGptImage1NumImages::One.to_edit(), E::One));
|
||||
assert!(matches!(ArtcraftGptImage1NumImages::Two.to_edit(), E::Two));
|
||||
assert!(matches!(ArtcraftGptImage1NumImages::Three.to_edit(), E::Three));
|
||||
assert!(matches!(ArtcraftGptImage1NumImages::Four.to_edit(), E::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_to_t2i_round_trip() {
|
||||
use GenerateGptImage1TextToImageImageQuality as T;
|
||||
assert!(matches!(ArtcraftGptImage1Quality::Auto.to_t2i(), T::Auto));
|
||||
assert!(matches!(ArtcraftGptImage1Quality::Low.to_t2i(), T::Low));
|
||||
assert!(matches!(ArtcraftGptImage1Quality::Medium.to_t2i(), T::Medium));
|
||||
assert!(matches!(ArtcraftGptImage1Quality::High.to_t2i(), T::High));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_to_edit_round_trip() {
|
||||
use GptImage1EditImageImageQuality as E;
|
||||
assert!(matches!(ArtcraftGptImage1Quality::Auto.to_edit(), E::Auto));
|
||||
assert!(matches!(ArtcraftGptImage1Quality::Low.to_edit(), E::Low));
|
||||
assert!(matches!(ArtcraftGptImage1Quality::Medium.to_edit(), E::Medium));
|
||||
assert!(matches!(ArtcraftGptImage1Quality::High.to_edit(), E::High));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_to_t2i_round_trip() {
|
||||
use GenerateGptImage1TextToImageImageSize as T;
|
||||
assert!(matches!(ArtcraftGptImage1Size::Square.to_t2i(), T::Square));
|
||||
assert!(matches!(ArtcraftGptImage1Size::Horizontal.to_t2i(), T::Horizontal));
|
||||
assert!(matches!(ArtcraftGptImage1Size::Vertical.to_t2i(), T::Vertical));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_to_edit_round_trip() {
|
||||
use GptImage1EditImageImageSize as E;
|
||||
assert!(matches!(ArtcraftGptImage1Size::Square.to_edit(), E::Square));
|
||||
assert!(matches!(ArtcraftGptImage1Size::Horizontal.to_edit(), E::Horizontal));
|
||||
assert!(matches!(ArtcraftGptImage1Size::Vertical.to_edit(), E::Vertical));
|
||||
}
|
||||
}
|
||||
+24
-12
@@ -1,4 +1,5 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
@@ -30,13 +31,14 @@ pub fn plan_generate_image_artcraft_gpt_image_1p5<'a>(
|
||||
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
|
||||
let quality = plan_quality(request.quality);
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftGptImage1p5(PlanArtcraftGptImage1p5 {
|
||||
prompt: request.prompt,
|
||||
image_inputs,
|
||||
image_size,
|
||||
quality: GptImage1p5MultiFunctionImageGenQuality::Medium,
|
||||
quality,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
}))
|
||||
@@ -48,9 +50,19 @@ fn resolve_image_list_ref<'a>(
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
// Omni-gen distillation hydrates media tokens to URLs before running the
|
||||
// Artcraft cost path. Cost only depends on quality + size + num_images, so
|
||||
// URL-form inputs are accepted and dropped.
|
||||
Some(ImageListRef::Urls(_)) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_quality(quality: Option<CommonQuality>) -> GptImage1p5MultiFunctionImageGenQuality {
|
||||
match quality {
|
||||
Some(CommonQuality::Low) => GptImage1p5MultiFunctionImageGenQuality::Low,
|
||||
Some(CommonQuality::Medium) => GptImage1p5MultiFunctionImageGenQuality::Medium,
|
||||
Some(CommonQuality::High) => GptImage1p5MultiFunctionImageGenQuality::High,
|
||||
None => GptImage1p5MultiFunctionImageGenQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +79,7 @@ fn plan_image_size(
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(None),
|
||||
|
||||
// Square
|
||||
@@ -195,10 +208,10 @@ mod tests {
|
||||
// ── Quality ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn quality_defaults_to_medium() {
|
||||
fn quality_defaults_to_high() {
|
||||
let request = base_gpt_image_1p5_image_request();
|
||||
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
|
||||
assert!(matches!(plan.quality, GptImage1p5MultiFunctionImageGenQuality::Medium));
|
||||
assert!(matches!(plan.quality, GptImage1p5MultiFunctionImageGenQuality::High));
|
||||
}
|
||||
|
||||
// ── Num images ──────────────────────────────────────────────────────────────
|
||||
@@ -277,17 +290,16 @@ mod tests {
|
||||
// ── Image inputs ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn url_image_inputs_returns_error() {
|
||||
fn url_image_inputs_are_accepted_for_cost_path() {
|
||||
let urls = vec!["https://example.com/image.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
..base_gpt_image_1p5_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1p5")
|
||||
};
|
||||
assert!(plan.image_inputs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+20
-9
@@ -45,9 +45,13 @@ fn resolve_image_list_ref<'a>(
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
// The omni-gen distillation path hydrates media tokens into URLs before
|
||||
// running cost estimation against the Artcraft provider. The cost only
|
||||
// depends on `num_images` and `is_edit_mode` (which is derived from
|
||||
// `request.image_inputs.is_some()` before this resolver runs), so URL-form
|
||||
// inputs are accepted here and dropped — there's no Artcraft execution
|
||||
// path for Nano Banana that would need to read them back.
|
||||
Some(ImageListRef::Urls(_)) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +294,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_nano_banana_image_request()
|
||||
@@ -323,6 +328,7 @@ mod tests {
|
||||
#[test]
|
||||
fn num_images_out_of_range_error_out() {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_nano_banana_image_request()
|
||||
@@ -341,6 +347,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_nano_banana_image_request()
|
||||
@@ -356,17 +363,21 @@ mod tests {
|
||||
// ── Image inputs ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn url_image_inputs_returns_error() {
|
||||
fn url_image_inputs_are_accepted_for_cost_path() {
|
||||
// The omni-gen distillation hydrates media tokens to URLs before running
|
||||
// cost estimation against Artcraft. URLs are accepted (and dropped) since
|
||||
// cost only depends on num_images + is_edit_mode.
|
||||
let urls = vec!["https://example.com/image.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
..base_nano_banana_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftNanaBanana")
|
||||
};
|
||||
// image_inputs is dropped (None), but edit-mode aspect-ratio handling
|
||||
// still kicks in because is_edit_mode is derived from request.image_inputs.
|
||||
assert!(plan.image_inputs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+10
-5
@@ -50,9 +50,12 @@ fn resolve_image_list_ref<'a>(
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
// The omni-gen distillation hydrates media tokens to URLs before running
|
||||
// cost estimation against the Artcraft provider. Cost only depends on
|
||||
// num_images + resolution + is_edit_mode (derived before this resolver
|
||||
// runs), so URL-form inputs are accepted and dropped — there's no Artcraft
|
||||
// execution path for Nano Banana 2 that would need to read them back.
|
||||
Some(ImageListRef::Urls(_)) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,8 +138,10 @@ fn plan_resolution(
|
||||
// No 3K option; default to 2K
|
||||
Ok(Some(Nb2Res::TwoK))
|
||||
}
|
||||
// HalfK, FourEightyP, SevenTwentyP, TenEightyP — nearest supported is OneK
|
||||
Some(R::HalfK | R::FourEightyP | R::SevenTwentyP | R::TenEightyP) => Ok(Some(Nb2Res::OneK)),
|
||||
// HalfK is natively supported (and billed at the cheaper 0.5K rate).
|
||||
Some(R::HalfK) => Ok(Some(Nb2Res::HalfK)),
|
||||
// FourEightyP, SevenTwentyP, TenEightyP — nearest supported is OneK
|
||||
Some(R::FourEightyP | R::SevenTwentyP | R::TenEightyP) => Ok(Some(Nb2Res::OneK)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-9
@@ -50,9 +50,12 @@ fn resolve_image_list_ref<'a>(
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
// The omni-gen distillation hydrates media tokens to URLs before running
|
||||
// cost estimation against the Artcraft provider. Cost only depends on
|
||||
// num_images + resolution + is_edit_mode (derived before this resolver
|
||||
// runs), so URL-form inputs are accepted and dropped — there's no Artcraft
|
||||
// execution path for Nano Banana Pro that would need to read them back.
|
||||
Some(ImageListRef::Urls(_)) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,6 +357,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_image_request()
|
||||
@@ -387,6 +391,7 @@ mod tests {
|
||||
#[test]
|
||||
fn num_images_out_of_range_error_out() {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_image_request()
|
||||
@@ -405,6 +410,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_image_request()
|
||||
@@ -420,17 +426,19 @@ mod tests {
|
||||
// ── Image inputs ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn url_image_inputs_returns_error() {
|
||||
fn url_image_inputs_are_accepted_for_cost_path() {
|
||||
// The omni-gen distillation hydrates media tokens to URLs before running
|
||||
// cost estimation against Artcraft. URLs are accepted (and dropped) since
|
||||
// cost only depends on num_images + resolution + is_edit_mode.
|
||||
let urls = vec!["https://example.com/image.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
..base_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftNanaBananaPro")
|
||||
};
|
||||
assert!(plan.image_inputs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+2
-1
@@ -82,7 +82,7 @@ fn plan_image_size(
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto4k) => {
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) | Some(CommonAspectRatio::Auto4k) => {
|
||||
Ok(Some(S::SquareHd))
|
||||
}
|
||||
|
||||
@@ -226,6 +226,7 @@ mod tests {
|
||||
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
|
||||
for (count, expected) in cases {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(count),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
|
||||
+13
-9
@@ -46,9 +46,10 @@ fn resolve_image_list_ref<'a>(
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
// Omni-gen distillation hydrates media tokens to URLs before running the
|
||||
// Artcraft cost path. Cost only depends on num_images, so URL-form inputs
|
||||
// are accepted and dropped.
|
||||
Some(ImageListRef::Urls(_)) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ fn plan_image_size(
|
||||
if is_edit_mode { Ok(Some(S::Auto)) } else { Ok(Some(S::Square)) }
|
||||
}
|
||||
|
||||
Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
|
||||
Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto2k)),
|
||||
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
|
||||
|
||||
// Square
|
||||
@@ -295,6 +296,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_seedream_4_image_request()
|
||||
@@ -322,6 +324,7 @@ mod tests {
|
||||
#[test]
|
||||
fn num_images_out_of_range_error_out() {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_seedream_4_image_request()
|
||||
@@ -336,6 +339,7 @@ mod tests {
|
||||
fn num_images_out_of_range_clamps_to_four() {
|
||||
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_seedream_4_image_request()
|
||||
@@ -348,16 +352,16 @@ mod tests {
|
||||
// ── Image inputs ──────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn url_image_inputs_returns_error() {
|
||||
fn url_image_inputs_are_accepted_for_cost_path() {
|
||||
let urls = vec!["https://example.com/image.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
..base_seedream_4_image_request()
|
||||
};
|
||||
assert!(matches!(
|
||||
request.build(),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftSeedream4")
|
||||
};
|
||||
assert!(plan.image_inputs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+13
-9
@@ -46,9 +46,10 @@ fn resolve_image_list_ref<'a>(
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
// Omni-gen distillation hydrates media tokens to URLs before running the
|
||||
// Artcraft cost path. Cost only depends on num_images, so URL-form inputs
|
||||
// are accepted and dropped.
|
||||
Some(ImageListRef::Urls(_)) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ fn plan_image_size(
|
||||
if is_edit_mode { Ok(Some(S::Auto2k)) } else { Ok(Some(S::Square)) }
|
||||
}
|
||||
|
||||
Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
|
||||
Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto2k)),
|
||||
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
|
||||
|
||||
// Square
|
||||
@@ -296,6 +297,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_seedream_4p5_image_request()
|
||||
@@ -323,6 +325,7 @@ mod tests {
|
||||
#[test]
|
||||
fn num_images_out_of_range_error_out() {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_seedream_4p5_image_request()
|
||||
@@ -337,6 +340,7 @@ mod tests {
|
||||
fn num_images_out_of_range_clamps_to_four() {
|
||||
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_seedream_4p5_image_request()
|
||||
@@ -349,16 +353,16 @@ mod tests {
|
||||
// ── Image inputs ──────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn url_image_inputs_returns_error() {
|
||||
fn url_image_inputs_are_accepted_for_cost_path() {
|
||||
let urls = vec!["https://example.com/image.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
..base_seedream_4p5_image_request()
|
||||
};
|
||||
assert!(matches!(
|
||||
request.build(),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftSeedream4p5")
|
||||
};
|
||||
assert!(plan.image_inputs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+5
-5
@@ -46,9 +46,10 @@ fn resolve_image_list_ref<'a>(
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
// Omni-gen distillation hydrates media tokens to URLs before running the
|
||||
// Artcraft cost path. Cost only depends on num_images, so URL-form inputs
|
||||
// are accepted and dropped.
|
||||
Some(ImageListRef::Urls(_)) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +73,7 @@ fn plan_image_size(
|
||||
}
|
||||
|
||||
Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
|
||||
Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto3k)),
|
||||
// 5 Lite max is auto_3K; map Auto4k down to Auto3k
|
||||
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto3k)),
|
||||
|
||||
@@ -147,8 +149,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::test_helpers::base_seedream_5_lite_image_request;
|
||||
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::{
|
||||
BytedanceSeedream5LiteMultiFunctionImageGenImageSize as S,
|
||||
|
||||
+7
@@ -1,5 +1,12 @@
|
||||
pub mod plan_generate_image_fal_flux_1_dev;
|
||||
pub mod plan_generate_image_fal_flux_1_schnell;
|
||||
pub mod plan_generate_image_fal_flux_pro_1p1;
|
||||
pub mod plan_generate_image_fal_flux_pro_1p1_ultra;
|
||||
pub mod plan_generate_image_fal_gpt_image_1;
|
||||
pub mod plan_generate_image_fal_gpt_image_1p5;
|
||||
pub mod plan_generate_image_fal_seedream_4;
|
||||
pub mod plan_generate_image_fal_seedream_4p5;
|
||||
pub mod plan_generate_image_fal_seedream_5_lite;
|
||||
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;
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
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::text::enqueue_flux_pro_11_text_to_image_webhook::{
|
||||
FluxPro11AspectRatio, FluxPro11NumImages,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalFluxPro11NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalFluxPro11<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub aspect_ratio: FluxPro11AspectRatio,
|
||||
pub num_images: FalFluxPro11NumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_flux_pro_1p1<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
if request.image_inputs.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: "Flux Pro 1.1 is text-to-image only".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalFluxPro11(PlanFalFluxPro11 {
|
||||
prompt: request.prompt,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FluxPro11AspectRatio, ArtcraftRouterError> {
|
||||
use FluxPro11AspectRatio 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<FalFluxPro11NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalFluxPro11NumImages::One),
|
||||
2 => Ok(FalFluxPro11NumImages::Two),
|
||||
3 => Ok(FalFluxPro11NumImages::Three),
|
||||
4 => Ok(FalFluxPro11NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalFluxPro11NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalFluxPro11NumImages {
|
||||
pub fn to_fal(self) -> FluxPro11NumImages {
|
||||
match self {
|
||||
Self::One => FluxPro11NumImages::One,
|
||||
Self::Two => FluxPro11NumImages::Two,
|
||||
Self::Three => FluxPro11NumImages::Three,
|
||||
Self::Four => FluxPro11NumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
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::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
|
||||
FluxPro11UltraAspectRatio, FluxPro11UltraNumImages,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalFluxPro11UltraNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalFluxPro11Ultra<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub aspect_ratio: FluxPro11UltraAspectRatio,
|
||||
pub num_images: FalFluxPro11UltraNumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_flux_pro_1p1_ultra<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
if request.image_inputs.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: "Flux Pro 1.1 Ultra is text-to-image only".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalFluxPro11Ultra(PlanFalFluxPro11Ultra {
|
||||
prompt: request.prompt,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FluxPro11UltraAspectRatio, ArtcraftRouterError> {
|
||||
use FluxPro11UltraAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Ar::LandscapeThreeByTwo),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Ar::LandscapeTwentyOneByNine),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Ar::PortraitTwoByThree),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::PortraitNineBySixteen),
|
||||
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Ar::PortraitNineByTwentyOne),
|
||||
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Ar::PortraitThreeByFour),
|
||||
|
||||
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<FalFluxPro11UltraNumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalFluxPro11UltraNumImages::One),
|
||||
2 => Ok(FalFluxPro11UltraNumImages::Two),
|
||||
3 => Ok(FalFluxPro11UltraNumImages::Three),
|
||||
4 => Ok(FalFluxPro11UltraNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalFluxPro11UltraNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalFluxPro11UltraNumImages {
|
||||
pub fn to_fal(self) -> FluxPro11UltraNumImages {
|
||||
match self {
|
||||
Self::One => FluxPro11UltraNumImages::One,
|
||||
Self::Two => FluxPro11UltraNumImages::Two,
|
||||
Self::Three => FluxPro11UltraNumImages::Three,
|
||||
Self::Four => FluxPro11UltraNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
+498
@@ -0,0 +1,498 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
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_gpt_image_1_edit_image_webhook::{
|
||||
EnqueueGptImage1EditImageNumImages, EnqueueGptImage1EditImageQuality,
|
||||
EnqueueGptImage1EditImageSize,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1_text_to_image_webhook::{
|
||||
EnqueueGptImage1TextToImageNumImages, EnqueueGptImage1TextToImageQuality,
|
||||
EnqueueGptImage1TextToImageSize,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalGptImage1NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
/// Quality is shared between t2i and edit (Low / Medium / High).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalGptImage1Quality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
/// GPT Image 1 supports three image sizes: 1024x1024, 1536x1024, 1024x1536.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalGptImage1ImageSize {
|
||||
Square,
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalGptImage1<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Image URLs for editing. Empty vec = text-to-image mode.
|
||||
pub image_urls: Vec<String>,
|
||||
pub image_size: Option<FalGptImage1ImageSize>,
|
||||
/// Quality defaults to Medium when the request leaves it unspecified.
|
||||
/// (Matches the fal cost calculator default.)
|
||||
pub quality: FalGptImage1Quality,
|
||||
pub num_images: FalGptImage1NumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_gpt_image_1<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let image_urls = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_size = plan_image_size(request.aspect_ratio);
|
||||
let quality = plan_quality(request.quality);
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalGptImage1(PlanFalGptImage1 {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
image_size,
|
||||
quality,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_quality(quality: Option<CommonQuality>) -> FalGptImage1Quality {
|
||||
match quality {
|
||||
Some(CommonQuality::Low) => FalGptImage1Quality::Low,
|
||||
Some(CommonQuality::Medium) => FalGptImage1Quality::Medium,
|
||||
Some(CommonQuality::High) => FalGptImage1Quality::High,
|
||||
None => FalGptImage1Quality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<FalGptImage1ImageSize> {
|
||||
use FalGptImage1ImageSize as S;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => None,
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Some(S::Square),
|
||||
|
||||
Some(CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(CommonAspectRatio::WideFourByThree)
|
||||
| Some(CommonAspectRatio::WideFiveByFour)
|
||||
| Some(CommonAspectRatio::WideSixteenByNine)
|
||||
| Some(CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(CommonAspectRatio::Wide) => Some(S::Horizontal),
|
||||
|
||||
Some(CommonAspectRatio::TallTwoByThree)
|
||||
| Some(CommonAspectRatio::TallThreeByFour)
|
||||
| Some(CommonAspectRatio::TallFourByFive)
|
||||
| Some(CommonAspectRatio::TallNineBySixteen)
|
||||
| Some(CommonAspectRatio::TallNineByTwentyOne)
|
||||
| Some(CommonAspectRatio::Tall) => Some(S::Vertical),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalGptImage1NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalGptImage1NumImages::One),
|
||||
2 => Ok(FalGptImage1NumImages::Two),
|
||||
3 => Ok(FalGptImage1NumImages::Three),
|
||||
4 => Ok(FalGptImage1NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalGptImage1NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── Conversions to fal-client enqueue enums ────────────────────────────────
|
||||
|
||||
impl FalGptImage1NumImages {
|
||||
pub fn to_t2i(self) -> EnqueueGptImage1TextToImageNumImages {
|
||||
use EnqueueGptImage1TextToImageNumImages as T;
|
||||
match self {
|
||||
Self::One => T::One,
|
||||
Self::Two => T::Two,
|
||||
Self::Three => T::Three,
|
||||
Self::Four => T::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueGptImage1EditImageNumImages {
|
||||
use EnqueueGptImage1EditImageNumImages as E;
|
||||
match self {
|
||||
Self::One => E::One,
|
||||
Self::Two => E::Two,
|
||||
Self::Three => E::Three,
|
||||
Self::Four => E::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalGptImage1Quality {
|
||||
pub fn to_t2i(self) -> EnqueueGptImage1TextToImageQuality {
|
||||
use EnqueueGptImage1TextToImageQuality as T;
|
||||
match self {
|
||||
Self::Low => T::Low,
|
||||
Self::Medium => T::Medium,
|
||||
Self::High => T::High,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueGptImage1EditImageQuality {
|
||||
use EnqueueGptImage1EditImageQuality as E;
|
||||
match self {
|
||||
Self::Low => E::Low,
|
||||
Self::Medium => E::Medium,
|
||||
Self::High => E::High,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalGptImage1ImageSize {
|
||||
pub fn to_t2i(self) -> EnqueueGptImage1TextToImageSize {
|
||||
use EnqueueGptImage1TextToImageSize as T;
|
||||
match self {
|
||||
Self::Square => T::Square,
|
||||
Self::Horizontal => T::Horizontal,
|
||||
Self::Vertical => T::Vertical,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueGptImage1EditImageSize {
|
||||
use EnqueueGptImage1EditImageSize as E;
|
||||
match self {
|
||||
Self::Square => E::Square,
|
||||
Self::Horizontal => E::Horizontal,
|
||||
Self::Vertical => E::Vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
|
||||
fn base_fal_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::GptImage1,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space"),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_plan<'a>(request: &'a GenerateImageRequest<'a>) -> PlanFalGptImage1<'a> {
|
||||
let ImageGenerationPlan::FalGptImage1(plan) =
|
||||
plan_generate_image_fal_gpt_image_1(request).expect("plan should succeed")
|
||||
else {
|
||||
panic!("expected FalGptImage1 variant")
|
||||
};
|
||||
plan
|
||||
}
|
||||
|
||||
// ── Mode detection ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn no_image_inputs_yields_empty_urls() {
|
||||
let request = GenerateImageRequest { image_inputs: None, ..base_fal_request() };
|
||||
let plan = build_plan(&request);
|
||||
assert!(plan.image_urls.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_image_inputs_are_extracted() {
|
||||
let urls = vec![
|
||||
"https://example.com/a.jpg".to_string(),
|
||||
"https://example.com/b.jpg".to_string(),
|
||||
];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
..base_fal_request()
|
||||
};
|
||||
let plan = build_plan(&request);
|
||||
assert_eq!(plan.image_urls, urls);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_token_inputs_return_error() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_fal_request()
|
||||
};
|
||||
let result = plan_generate_image_fal_gpt_image_1(&request);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
|
||||
// ── Quality default ──────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn quality_defaults_to_high() {
|
||||
let request = base_fal_request();
|
||||
let plan = build_plan(&request);
|
||||
assert!(matches!(plan.quality, FalGptImage1Quality::High));
|
||||
}
|
||||
|
||||
// ── Image size mapping ───────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn image_size_none_is_none() {
|
||||
let request = GenerateImageRequest { aspect_ratio: None, ..base_fal_request() };
|
||||
let plan = build_plan(&request);
|
||||
assert!(plan.image_size.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_auto_variants_yield_none() {
|
||||
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
|
||||
let request = GenerateImageRequest { aspect_ratio: Some(auto_ar), ..base_fal_request() };
|
||||
let plan = build_plan(&request);
|
||||
assert!(plan.image_size.is_none(), "expected None for {:?}", auto_ar);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_square_variants() {
|
||||
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
|
||||
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_fal_request() };
|
||||
let plan = build_plan(&request);
|
||||
assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Square)), "expected Square for {:?}", ar);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_wide_variants_yield_horizontal() {
|
||||
let wide_ars = [
|
||||
CommonAspectRatio::WideThreeByTwo,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::WideFiveByFour,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::Wide,
|
||||
];
|
||||
for ar in wide_ars {
|
||||
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_fal_request() };
|
||||
let plan = build_plan(&request);
|
||||
assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal)), "expected Horizontal for {:?}", ar);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_tall_variants_yield_vertical() {
|
||||
let tall_ars = [
|
||||
CommonAspectRatio::TallTwoByThree,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallFourByFive,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
CommonAspectRatio::TallNineByTwentyOne,
|
||||
CommonAspectRatio::Tall,
|
||||
];
|
||||
for ar in tall_ars {
|
||||
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_fal_request() };
|
||||
let plan = build_plan(&request);
|
||||
assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical)), "expected Vertical for {:?}", ar);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Num images ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn num_images_zero_is_always_error() {
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::ErrorOut,
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_fal_request()
|
||||
};
|
||||
let result = plan_generate_image_fal_gpt_image_1(&request);
|
||||
assert!(
|
||||
matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
|
||||
"expected UserRequestedZeroGenerations with {:?}", strategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_default_is_one() {
|
||||
let request = GenerateImageRequest { image_batch_count: None, ..base_fal_request() };
|
||||
let plan = build_plan(&request);
|
||||
assert!(matches!(plan.num_images, FalGptImage1NumImages::One));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_direct_mapping() {
|
||||
let cases = [
|
||||
(1, FalGptImage1NumImages::One),
|
||||
(2, FalGptImage1NumImages::Two),
|
||||
(3, FalGptImage1NumImages::Three),
|
||||
(4, FalGptImage1NumImages::Four),
|
||||
];
|
||||
for (count, expected) in cases {
|
||||
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_fal_request() };
|
||||
let plan = build_plan(&request);
|
||||
assert!(
|
||||
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
|
||||
"expected {:?} for count {}", expected, count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_out_of_range_error_out() {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_fal_request()
|
||||
};
|
||||
let result = plan_generate_image_fal_gpt_image_1(&request);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_out_of_range_clamps_to_four() {
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(9),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_fal_request()
|
||||
};
|
||||
let plan = build_plan(&request);
|
||||
assert!(
|
||||
matches!(plan.num_images, FalGptImage1NumImages::Four),
|
||||
"expected Four for count 9 with {:?}", strategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── as_u64 ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn num_images_as_u64() {
|
||||
assert_eq!(FalGptImage1NumImages::One.as_u64(), 1);
|
||||
assert_eq!(FalGptImage1NumImages::Two.as_u64(), 2);
|
||||
assert_eq!(FalGptImage1NumImages::Three.as_u64(), 3);
|
||||
assert_eq!(FalGptImage1NumImages::Four.as_u64(), 4);
|
||||
}
|
||||
|
||||
// ── Conversions to fal-client enqueue enums ─────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn num_images_to_t2i_round_trip() {
|
||||
use EnqueueGptImage1TextToImageNumImages as T;
|
||||
assert!(matches!(FalGptImage1NumImages::One.to_t2i(), T::One));
|
||||
assert!(matches!(FalGptImage1NumImages::Two.to_t2i(), T::Two));
|
||||
assert!(matches!(FalGptImage1NumImages::Three.to_t2i(), T::Three));
|
||||
assert!(matches!(FalGptImage1NumImages::Four.to_t2i(), T::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_to_edit_round_trip() {
|
||||
use EnqueueGptImage1EditImageNumImages as E;
|
||||
assert!(matches!(FalGptImage1NumImages::One.to_edit(), E::One));
|
||||
assert!(matches!(FalGptImage1NumImages::Two.to_edit(), E::Two));
|
||||
assert!(matches!(FalGptImage1NumImages::Three.to_edit(), E::Three));
|
||||
assert!(matches!(FalGptImage1NumImages::Four.to_edit(), E::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_to_t2i_round_trip() {
|
||||
use EnqueueGptImage1TextToImageQuality as T;
|
||||
assert!(matches!(FalGptImage1Quality::Low.to_t2i(), T::Low));
|
||||
assert!(matches!(FalGptImage1Quality::Medium.to_t2i(), T::Medium));
|
||||
assert!(matches!(FalGptImage1Quality::High.to_t2i(), T::High));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_to_edit_round_trip() {
|
||||
use EnqueueGptImage1EditImageQuality as E;
|
||||
assert!(matches!(FalGptImage1Quality::Low.to_edit(), E::Low));
|
||||
assert!(matches!(FalGptImage1Quality::Medium.to_edit(), E::Medium));
|
||||
assert!(matches!(FalGptImage1Quality::High.to_edit(), E::High));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_to_t2i_round_trip() {
|
||||
use EnqueueGptImage1TextToImageSize as T;
|
||||
assert!(matches!(FalGptImage1ImageSize::Square.to_t2i(), T::Square));
|
||||
assert!(matches!(FalGptImage1ImageSize::Horizontal.to_t2i(), T::Horizontal));
|
||||
assert!(matches!(FalGptImage1ImageSize::Vertical.to_t2i(), T::Vertical));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_to_edit_round_trip() {
|
||||
use EnqueueGptImage1EditImageSize as E;
|
||||
assert!(matches!(FalGptImage1ImageSize::Square.to_edit(), E::Square));
|
||||
assert!(matches!(FalGptImage1ImageSize::Horizontal.to_edit(), E::Horizontal));
|
||||
assert!(matches!(FalGptImage1ImageSize::Vertical.to_edit(), E::Vertical));
|
||||
}
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
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_gpt_image_1p5_edit_image_webhook::{
|
||||
EnqueueGptImage1p5EditImageNumImages, EnqueueGptImage1p5EditImageQuality,
|
||||
EnqueueGptImage1p5EditImageSize,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1p5_text_to_image_webhook::{
|
||||
EnqueueGptImage1p5TextToImageNumImages, EnqueueGptImage1p5TextToImageQuality,
|
||||
EnqueueGptImage1p5TextToImageSize,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalGptImage1p5NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
/// Quality is shared between t2i and edit (Low / Medium / High).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalGptImage1p5Quality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
/// GPT Image 1.5 supports three image sizes: 1024x1024, 1536x1024, 1024x1536.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalGptImage1p5ImageSize {
|
||||
Square,
|
||||
Wide,
|
||||
Tall,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalGptImage1p5<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub image_urls: Vec<String>,
|
||||
pub image_size: Option<FalGptImage1p5ImageSize>,
|
||||
pub quality: FalGptImage1p5Quality,
|
||||
pub num_images: FalGptImage1p5NumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_gpt_image_1p5<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let image_urls = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_size = plan_image_size(request.aspect_ratio);
|
||||
let quality = plan_quality(request.quality);
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalGptImage1p5(PlanFalGptImage1p5 {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
image_size,
|
||||
quality,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_quality(quality: Option<CommonQuality>) -> FalGptImage1p5Quality {
|
||||
match quality {
|
||||
Some(CommonQuality::Low) => FalGptImage1p5Quality::Low,
|
||||
Some(CommonQuality::Medium) => FalGptImage1p5Quality::Medium,
|
||||
Some(CommonQuality::High) => FalGptImage1p5Quality::High,
|
||||
None => FalGptImage1p5Quality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<FalGptImage1p5ImageSize> {
|
||||
use FalGptImage1p5ImageSize as S;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => None,
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Some(S::Square),
|
||||
|
||||
Some(CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(CommonAspectRatio::WideFourByThree)
|
||||
| Some(CommonAspectRatio::WideFiveByFour)
|
||||
| Some(CommonAspectRatio::WideSixteenByNine)
|
||||
| Some(CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(CommonAspectRatio::Wide) => Some(S::Wide),
|
||||
|
||||
Some(CommonAspectRatio::TallTwoByThree)
|
||||
| Some(CommonAspectRatio::TallThreeByFour)
|
||||
| Some(CommonAspectRatio::TallFourByFive)
|
||||
| Some(CommonAspectRatio::TallNineBySixteen)
|
||||
| Some(CommonAspectRatio::TallNineByTwentyOne)
|
||||
| Some(CommonAspectRatio::Tall) => Some(S::Tall),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalGptImage1p5NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalGptImage1p5NumImages::One),
|
||||
2 => Ok(FalGptImage1p5NumImages::Two),
|
||||
3 => Ok(FalGptImage1p5NumImages::Three),
|
||||
4 => Ok(FalGptImage1p5NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalGptImage1p5NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalGptImage1p5NumImages {
|
||||
pub fn to_t2i(self) -> EnqueueGptImage1p5TextToImageNumImages {
|
||||
use EnqueueGptImage1p5TextToImageNumImages as T;
|
||||
match self {
|
||||
Self::One => T::One,
|
||||
Self::Two => T::Two,
|
||||
Self::Three => T::Three,
|
||||
Self::Four => T::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueGptImage1p5EditImageNumImages {
|
||||
use EnqueueGptImage1p5EditImageNumImages as E;
|
||||
match self {
|
||||
Self::One => E::One,
|
||||
Self::Two => E::Two,
|
||||
Self::Three => E::Three,
|
||||
Self::Four => E::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalGptImage1p5Quality {
|
||||
pub fn to_t2i(self) -> EnqueueGptImage1p5TextToImageQuality {
|
||||
use EnqueueGptImage1p5TextToImageQuality as T;
|
||||
match self {
|
||||
Self::Low => T::Low,
|
||||
Self::Medium => T::Medium,
|
||||
Self::High => T::High,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueGptImage1p5EditImageQuality {
|
||||
use EnqueueGptImage1p5EditImageQuality as E;
|
||||
match self {
|
||||
Self::Low => E::Low,
|
||||
Self::Medium => E::Medium,
|
||||
Self::High => E::High,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalGptImage1p5ImageSize {
|
||||
pub fn to_t2i(self) -> EnqueueGptImage1p5TextToImageSize {
|
||||
use EnqueueGptImage1p5TextToImageSize as T;
|
||||
match self {
|
||||
Self::Square => T::Square,
|
||||
Self::Wide => T::Wide,
|
||||
Self::Tall => T::Tall,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueGptImage1p5EditImageSize {
|
||||
use EnqueueGptImage1p5EditImageSize as E;
|
||||
match self {
|
||||
Self::Square => E::Square,
|
||||
Self::Wide => E::Wide,
|
||||
Self::Tall => E::Tall,
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -256,6 +256,7 @@ mod tests {
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
@@ -478,7 +479,8 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(0),
|
||||
quality: None,
|
||||
image_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_fal_request()
|
||||
};
|
||||
@@ -511,6 +513,7 @@ mod tests {
|
||||
#[test]
|
||||
fn num_images_out_of_range_error_out() {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_fal_request()
|
||||
@@ -529,7 +532,8 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(5),
|
||||
quality: None,
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_fal_request()
|
||||
};
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
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_bytedance_seedream_v4_edit_image_webhook::{
|
||||
EnqueueBytedanceSeedreamV4EditImageNumImages, EnqueueBytedanceSeedreamV4EditImageSize,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{
|
||||
EnqueueBytedanceSeedreamV4TextToImageNumImages, EnqueueBytedanceSeedreamV4TextToImageSize,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalSeedream4NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
/// Shared image-size shape between Seedream V4 t2i and edit (variants are identical).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalSeedream4ImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
Auto,
|
||||
Auto2k,
|
||||
Auto4k,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalSeedream4<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Image URLs for editing. Empty vec = text-to-image mode.
|
||||
pub image_urls: Vec<String>,
|
||||
pub image_size: Option<FalSeedream4ImageSize>,
|
||||
pub num_images: FalSeedream4NumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_seedream_4<'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 image_size = plan_image_size(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalSeedream4(PlanFalSeedream4 {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
image_size,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
is_edit_mode: bool,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<FalSeedream4ImageSize>, ArtcraftRouterError> {
|
||||
use FalSeedream4ImageSize as S;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto) => {
|
||||
if is_edit_mode { Ok(Some(S::Auto)) } else { Ok(Some(S::Square)) }
|
||||
}
|
||||
Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto2k)),
|
||||
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
|
||||
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::LandscapeSixteenNine)),
|
||||
},
|
||||
|
||||
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::TallFourByFive)
|
||||
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
|
||||
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::PortraitSixteenNine)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalSeedream4NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalSeedream4NumImages::One),
|
||||
2 => Ok(FalSeedream4NumImages::Two),
|
||||
3 => Ok(FalSeedream4NumImages::Three),
|
||||
4 => Ok(FalSeedream4NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalSeedream4NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalSeedream4NumImages {
|
||||
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV4TextToImageNumImages {
|
||||
use EnqueueBytedanceSeedreamV4TextToImageNumImages as T;
|
||||
match self {
|
||||
Self::One => T::One,
|
||||
Self::Two => T::Two,
|
||||
Self::Three => T::Three,
|
||||
Self::Four => T::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV4EditImageNumImages {
|
||||
use EnqueueBytedanceSeedreamV4EditImageNumImages as E;
|
||||
match self {
|
||||
Self::One => E::One,
|
||||
Self::Two => E::Two,
|
||||
Self::Three => E::Three,
|
||||
Self::Four => E::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalSeedream4ImageSize {
|
||||
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV4TextToImageSize {
|
||||
use EnqueueBytedanceSeedreamV4TextToImageSize as T;
|
||||
match self {
|
||||
Self::Square => T::Square,
|
||||
Self::SquareHd => T::SquareHd,
|
||||
Self::PortraitFourThree => T::PortraitFourThree,
|
||||
Self::PortraitSixteenNine => T::PortraitSixteenNine,
|
||||
Self::LandscapeFourThree => T::LandscapeFourThree,
|
||||
Self::LandscapeSixteenNine => T::LandscapeSixteenNine,
|
||||
Self::Auto => T::Auto,
|
||||
Self::Auto2k => T::Auto2k,
|
||||
Self::Auto4k => T::Auto4k,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV4EditImageSize {
|
||||
use EnqueueBytedanceSeedreamV4EditImageSize as E;
|
||||
match self {
|
||||
Self::Square => E::Square,
|
||||
Self::SquareHd => E::SquareHd,
|
||||
Self::PortraitFourThree => E::PortraitFourThree,
|
||||
Self::PortraitSixteenNine => E::PortraitSixteenNine,
|
||||
Self::LandscapeFourThree => E::LandscapeFourThree,
|
||||
Self::LandscapeSixteenNine => E::LandscapeSixteenNine,
|
||||
Self::Auto => E::Auto,
|
||||
Self::Auto2k => E::Auto2k,
|
||||
Self::Auto4k => E::Auto4k,
|
||||
}
|
||||
}
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
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_bytedance_seedream_v4p5_edit_image_webhook::{
|
||||
EnqueueBytedanceSeedreamV4p5EditImageNumImages, EnqueueBytedanceSeedreamV4p5EditImageSize,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageNumImages, EnqueueBytedanceSeedreamV4p5TextToImageSize,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalSeedream4p5NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
/// Note: Seedream V4.5 lacks the bare "Auto" image_size; only Auto2k/Auto4k are supported.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalSeedream4p5ImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
Auto2k,
|
||||
Auto4k,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalSeedream4p5<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub image_urls: Vec<String>,
|
||||
pub image_size: Option<FalSeedream4p5ImageSize>,
|
||||
pub num_images: FalSeedream4p5NumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_seedream_4p5<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let image_urls = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalSeedream4p5(PlanFalSeedream4p5 {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
image_size,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<FalSeedream4p5ImageSize>, ArtcraftRouterError> {
|
||||
use FalSeedream4p5ImageSize as S;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
// No bare Auto for v4.5 — fall back to Auto2k.
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto2k)),
|
||||
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
|
||||
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::LandscapeSixteenNine)),
|
||||
},
|
||||
|
||||
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::TallFourByFive)
|
||||
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
|
||||
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::PortraitSixteenNine)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalSeedream4p5NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalSeedream4p5NumImages::One),
|
||||
2 => Ok(FalSeedream4p5NumImages::Two),
|
||||
3 => Ok(FalSeedream4p5NumImages::Three),
|
||||
4 => Ok(FalSeedream4p5NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalSeedream4p5NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalSeedream4p5NumImages {
|
||||
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV4p5TextToImageNumImages {
|
||||
use EnqueueBytedanceSeedreamV4p5TextToImageNumImages as T;
|
||||
match self {
|
||||
Self::One => T::One,
|
||||
Self::Two => T::Two,
|
||||
Self::Three => T::Three,
|
||||
Self::Four => T::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV4p5EditImageNumImages {
|
||||
use EnqueueBytedanceSeedreamV4p5EditImageNumImages as E;
|
||||
match self {
|
||||
Self::One => E::One,
|
||||
Self::Two => E::Two,
|
||||
Self::Three => E::Three,
|
||||
Self::Four => E::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalSeedream4p5ImageSize {
|
||||
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV4p5TextToImageSize {
|
||||
use EnqueueBytedanceSeedreamV4p5TextToImageSize as T;
|
||||
match self {
|
||||
Self::Square => T::Square,
|
||||
Self::SquareHd => T::SquareHd,
|
||||
Self::PortraitFourThree => T::PortraitFourThree,
|
||||
Self::PortraitSixteenNine => T::PortraitSixteenNine,
|
||||
Self::LandscapeFourThree => T::LandscapeFourThree,
|
||||
Self::LandscapeSixteenNine => T::LandscapeSixteenNine,
|
||||
Self::Auto2k => T::Auto2k,
|
||||
Self::Auto4k => T::Auto4k,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV4p5EditImageSize {
|
||||
use EnqueueBytedanceSeedreamV4p5EditImageSize as E;
|
||||
match self {
|
||||
Self::Square => E::Square,
|
||||
Self::SquareHd => E::SquareHd,
|
||||
Self::PortraitFourThree => E::PortraitFourThree,
|
||||
Self::PortraitSixteenNine => E::PortraitSixteenNine,
|
||||
Self::LandscapeFourThree => E::LandscapeFourThree,
|
||||
Self::LandscapeSixteenNine => E::LandscapeSixteenNine,
|
||||
Self::Auto2k => E::Auto2k,
|
||||
Self::Auto4k => E::Auto4k,
|
||||
}
|
||||
}
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
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_bytedance_seedream_v5_lite_edit_image_webhook::{
|
||||
EnqueueBytedanceSeedreamV5LiteEditImageNumImages, EnqueueBytedanceSeedreamV5LiteEditImageSize,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::{
|
||||
EnqueueBytedanceSeedreamV5LiteTextToImageNumImages, EnqueueBytedanceSeedreamV5LiteTextToImageSize,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalSeedream5LiteNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
/// Note: Seedream V5 Lite supports Auto2k and Auto3k (no 4K, no bare Auto).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalSeedream5LiteImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
Auto2k,
|
||||
Auto3k,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalSeedream5Lite<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub image_urls: Vec<String>,
|
||||
pub image_size: Option<FalSeedream5LiteImageSize>,
|
||||
pub num_images: FalSeedream5LiteNumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_seedream_5_lite<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let image_urls = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, request.resolution, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalSeedream5Lite(PlanFalSeedream5Lite {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
image_size,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<FalSeedream5LiteImageSize>, ArtcraftRouterError> {
|
||||
use FalSeedream5LiteImageSize as S;
|
||||
|
||||
// Resolution-based auto sizes take precedence when an explicit Auto* aspect ratio
|
||||
// isn't specified but a resolution is — Seedream v5 lite has no 4K, fall back to 3K.
|
||||
match aspect_ratio {
|
||||
None => match resolution {
|
||||
Some(CommonResolution::TwoK) => Ok(Some(S::Auto2k)),
|
||||
Some(CommonResolution::ThreeK) | Some(CommonResolution::FourK) => Ok(Some(S::Auto3k)),
|
||||
_ => Ok(None),
|
||||
},
|
||||
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
|
||||
Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto3k)),
|
||||
// No 4K — fall back to 3K.
|
||||
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto3k)),
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
|
||||
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::LandscapeSixteenNine)),
|
||||
},
|
||||
|
||||
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::TallFourByFive)
|
||||
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
|
||||
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::PortraitSixteenNine)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalSeedream5LiteNumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalSeedream5LiteNumImages::One),
|
||||
2 => Ok(FalSeedream5LiteNumImages::Two),
|
||||
3 => Ok(FalSeedream5LiteNumImages::Three),
|
||||
4 => Ok(FalSeedream5LiteNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalSeedream5LiteNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalSeedream5LiteNumImages {
|
||||
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV5LiteTextToImageNumImages {
|
||||
use EnqueueBytedanceSeedreamV5LiteTextToImageNumImages as T;
|
||||
match self {
|
||||
Self::One => T::One,
|
||||
Self::Two => T::Two,
|
||||
Self::Three => T::Three,
|
||||
Self::Four => T::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV5LiteEditImageNumImages {
|
||||
use EnqueueBytedanceSeedreamV5LiteEditImageNumImages as E;
|
||||
match self {
|
||||
Self::One => E::One,
|
||||
Self::Two => E::Two,
|
||||
Self::Three => E::Three,
|
||||
Self::Four => E::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalSeedream5LiteImageSize {
|
||||
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV5LiteTextToImageSize {
|
||||
use EnqueueBytedanceSeedreamV5LiteTextToImageSize as T;
|
||||
match self {
|
||||
Self::Square => T::Square,
|
||||
Self::SquareHd => T::SquareHd,
|
||||
Self::PortraitFourThree => T::PortraitFourThree,
|
||||
Self::PortraitSixteenNine => T::PortraitSixteenNine,
|
||||
Self::LandscapeFourThree => T::LandscapeFourThree,
|
||||
Self::LandscapeSixteenNine => T::LandscapeSixteenNine,
|
||||
Self::Auto2k => T::Auto2k,
|
||||
Self::Auto3k => T::Auto3k,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV5LiteEditImageSize {
|
||||
use EnqueueBytedanceSeedreamV5LiteEditImageSize as E;
|
||||
match self {
|
||||
Self::Square => E::Square,
|
||||
Self::SquareHd => E::SquareHd,
|
||||
Self::PortraitFourThree => E::PortraitFourThree,
|
||||
Self::PortraitSixteenNine => E::PortraitSixteenNine,
|
||||
Self::LandscapeFourThree => E::LandscapeFourThree,
|
||||
Self::LandscapeSixteenNine => E::LandscapeSixteenNine,
|
||||
Self::Auto2k => E::Auto2k,
|
||||
Self::Auto3k => E::Auto3k,
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_1_6_pro::PlanArtcraftKling16Pro;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
pub(crate) fn estimate_video_cost_artcraft_kling_1_6_pro(
|
||||
plan: &PlanArtcraftKling16Pro<'_>,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
// Mirrors fal_client kling_v1p6_pro: 5s = $0.475 (rounded up to 48¢), 10s = $0.95 (95¢).
|
||||
let cost_in_usd_cents: u64 = if plan.is_ten_seconds() { 95 } else { 48 };
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
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,
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_2_1_master::PlanArtcraftKling21Master;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
pub(crate) fn estimate_video_cost_artcraft_kling_2_1_master(
|
||||
plan: &PlanArtcraftKling21Master<'_>,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
// Mirrors fal_client kling_v2p1_master: 5s = $1.40, 10s = $2.80.
|
||||
let cost_in_usd_cents: u64 = if plan.is_ten_seconds() { 280 } else { 140 };
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
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,
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_2_1_pro::PlanArtcraftKling21Pro;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
pub(crate) fn estimate_video_cost_artcraft_kling_2_1_pro(
|
||||
plan: &PlanArtcraftKling21Pro<'_>,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
// Mirrors fal_client kling_v2p1_pro: 5s = $0.45, 10s = $0.90.
|
||||
let cost_in_usd_cents: u64 = if plan.is_ten_seconds() { 90 } else { 45 };
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
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,
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_2_5_turbo_pro::PlanArtcraftKling2p5TurboPro;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
pub(crate) fn estimate_video_cost_artcraft_kling_2_5_turbo_pro(
|
||||
plan: &PlanArtcraftKling2p5TurboPro<'_>,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
// Mirrors fal_client kling_v2p5_turbo_pro: 5s = $0.35, 10s = $0.70.
|
||||
let cost_in_usd_cents: u64 = if plan.is_ten_seconds() { 70 } else { 35 };
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
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,
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user